drewolson

drewolson

Building a process to consume an HTTP2 stream of events and handle graceful shutdown during testing

I’m attempting to model the following problem with a process (or set of processes) – I need to consume an HTTP2 streaming API and enqueue Oban jobs based on the events from this API.

I’m using Oban for database-backed jobs and Finch as a higher-level wrapper around Mint for my HTTP client.

I’d also like to be able to test this process by spinning it up from an ExUnit test, ensuring it enqueues some Oban jobs, and then have it spin down gracefully after the text exits.

So far, I haven’t been able to find a suitable solution. One of the primary problems is that, when my test attempts to shutdown, my process may still be communicating with the database, attempting to enqueue jobs via Oban.

Here are some thoughts I’ve had about modeling this:

  1. It should probably be done via a GenServer
  2. Initially I thought the “looping” code that pulls messages from the HTTP2 stream could be done in a handle_info callback, but the API provided by finch effectively “blocks” the GenServer from receiving other messages while the connection is open. Therefore, it seems my GenServer should probably spin up a supervised task to read from the API and send messages back to the GenServer for processing.
  3. In order to exit gracefully, I need to know if the server should be attempting to process messages it receives or ignore them because it is in the process of shutting down.
  4. I want to attempt to handle this graceful shutdown via the terminate callback, as I’ll be starting my GenServer via start_supervised in my tests and I don’t want to require specialized cleanup code.

Given these constraints, I thought I had a solution. Here’s a sketch of what I thought might work:

defmodule WorkerExperiment.Worker do
  use GenServer

  def start_link(_args, opts \\ []) do
    GenServer.start_link(__MODULE__, nil, opts)
  end

  @impl GenServer
  def init(_) do
    Process.flag(:trap_exit, true)

    {:ok, :running, {:continue, :start_stream}}
  end

  @impl GenServer
  def handle_continue(:start_stream, state) do
    Task.Supervisor.start_child(
      WorkerExperiment.TaskSupervisor,
      __MODULE__,
      :stream,
      [self()]
    )

    {:noreply, state}
  end

  @impl GenServer
  def terminate(_reason, state) do
    :ok = GenServer.call(self(), :stop)

    state
  end

  @impl GenServer
  def handle_call(:stop, _from, _state), do: {:reply, :ok, :stopped}

  @impl GenServer
  def handle_info({:process_message, _}, :stopped), do: {:noreply, :stopped}

  def handle_info({:process_message, i}, :running) do
    # Oban insertion should happen here
    IO.inspect("Processing message #{i}")

    {:noreply, :running}
  end

  def stream(pid) do
    WorkerExperiment.HttpStream.stream(0, fn i, acc ->
      send(pid, {:process_message, i})
      acc + i
    end)
  end
end

Note that WorkerExperiment.HttpStream.stream is just a dummy function that reduces over an infinite stream, attempting to replicate the idea of keeping an HTTP2 connection open for (potentially) a very long time, with roughly the same API provided by Finch.

This almost does the trick, but alas I learned the hard way that GenServer’s cannot cast messages to themselves. My understanding is that the action that tells the server to “stop processing” must be sync, in order to solve the “severing-DB-connection-issue” referenced in the Ecto.Adapters.SQL.Sandbox FAQ.

What am I missing here? Is there a better way to model this problem that solves all of the following:

  1. Consumes messages from the HTTP2 stream
  2. Atomically inserts the messages for processing into the database via Oban
  3. Gracefully shuts down during testing
  4. Doesn’t require manually calling GenServer.call(pid, :some_message) at the end of my test to shut down cleanly, but rather leverages the supervision tree via start_supervised and hooks into the normal OTP lifecycle of shutdown

Thanks!

Marked As Solved

sorentwo

sorentwo

Oban Core Team

Ah, it is using an async send so there isn’t any backpressure. That would surely flood the mailbox. Switching from handle_info to a handle_call to make it synchronous should help.

Also Liked

sorentwo

sorentwo

Oban Core Team

This is a pretty common issue when working with GenServers that interact with Ecto independently. You’ll end up with a race condition between the point where the process checks out a connection and executes it, and when your process is stopped by the test. The connections “owner” is your GenServer process which no longer exists, hence the error message.

The good news is that it is only a problem when testing due to the Ecto sandbox. If you’re set on preventing it you’ll need to put in something that blocks that process from shutting down until it is finished processing.

After reading through the GH example I think it could be as simple as shutting down the task within terminate/2 (not sure if this translates to the actual Finch code):

  @impl GenServer
  def init(_) do
    state = %{mode: :running, task_pid: nil}

    {:ok, state, {:continue, :start_stream}}
  end

  @impl GenServer
  def handle_continue(:start_stream, state) do
    {:ok, pid} = Task.start_link(__MODULE__, :stream, [self()])

    {:noreply, %{state | task_pid: pid}}
  end

  @impl GenServer
  def terminate(_reason, state) do
    if is_pid(state.task_pid), do: Task.shutdown(state.task_pid)

    :ok
  end

Where Next?

Popular in Questions Top

itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
hpopp
To simplify some tasks at work, I wrote and published this package yesterday. It’s a simple macro that enables Access behaviour on struct...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New

We're in Beta

About us Mission Statement