sprql

sprql

What is proper way to handle messages when connection is not ready?

I’m working on an app which receives messages via HTTP and sends it to RabbitMQ. So, I have GenServer which handles connection and publishing to a rabbit.

I’m curious about a proper approach to implement buffer/message queue to handle messages while
connection to rabbit is not ready or in a reconnection loop.

To implement rabbit handler I use https://github.com/fishcakez/connection behavior and amqp:

defmodule Rabbit do
  use Connection
  ...
end

One of the ways to handle messages while a connection is not ready is to use Erlang queue module inside Rabbit module, like this:

def handle_call({:publish, message}, _from, %{conn: nil, queue: queue} = state) do
  new_queue = :queue.in(message, queue)
  {:reply, :ok, %{state | queue: new_queue}}
end

def connect(_, state) do
  with conn <- make_connection() do
    state = %{state | conn: conn}
    dequeue(state)
  else
    # error handling
  end
end

def dequeue(state) do
  case :queue.out(state.queue) do
    {{:value, message}, new_queue} ->
      publish_message(state.conn, message)
      dequeue(%{state | queue: new_queue})
    {:empty, _} -> state
  end
end

So, is it a good approach? Or should I use dedicated GenServer for queue? Or maybe something else.

Most Liked

DavidAntaramian

DavidAntaramian

I have a few suggestions based on what you’ve said so far, and I hope they help. Reading through your description, you are first sending all messages to a GenServer, Rabbit, which is responsible for maintaining a connection to a RabbitMQ server and acting as a message buffer for the upstream service. You may have done this intentionally, but this raises two concerns for me:

  • Your message buffer is directly associated with the health of the connection. If the connection encounters an error, you lose your message buffer.
  • You’ve serialized the message dispatch to Rabbit; this creates an artificial bottleneck in your code that you don’t need unless you must send the messages in the order they were received by the GenServer (which is unlikely given that it would be difficult to guarantee their order in the first place)

To put this more bluntly, Rabbit has two responsibilities in its state: the connection and the message queue.

The Connection behaviour is specifically designed to help manage a connection to a remote service, and the documentation is very useful in understanding how to start a connection when the process is started (by returning {:connect, info, state} inside of init/1). This is a good use of a GenServer process because the connection and connection state are held in the GenServer state. The GenServer can then give informative responses to callers.

However, I don’t see any reason in your description to have a buffer. I would remove the buffer part completely and have the processes handling the HTTP processes responsible for calling Rabbit. Because Rabbit uses Connection, even when the connection to the Rabbit server isn’t available, it can handle messages from callers and inform them, with a response like {:reply, :noconnect, state}. The caller can then make a decision about whether to retry or inform the downstream client that there was an error.

The model I described above has a few benefits:

  • There is no longer any message queue to deal with, so you aren’t dealing with the potential to “lose everything” that was in a GenServer state because of minor errors
  • The success state is pushed farther downstream, towards the client, and the client is now in agreement about the state of the message. This empowers the end user to make a decision to try again or not, and it also avoids a situation where the end user was informed something succeeded only for it to fail silently somewhere else.

For the model I described above, in the supervision tree, Rabbit should be at the far left, and the HTTP processes should be to the right of Rabbit (possibly as children of an HTTP process supervisor). This ensures Rabbit is started and allowed to connect before HTTP requests are handled. Also, in the case that Rabbit suffers irrecoverable issues and isn’t able to maintain a connection, this will force the HTTP processes to restart.

The model above still suffers an inherent flaw, though: the GenServer will only processes messages serially, so all HTTP connections will have to share the single Rabbit connection. If your HTTP requests pile up faster than your connection can handle them, the calls will timeout. As your service grows, you may instead require a pool of Rabbit connections, in which case the RabbitPool would be at the far left in the same manner as described above, but the HTTP processes must now check out a Rabbit connection from the pool before performing operations. This allows you to service multiple HTTP requests concurrently.

Overall, I think that would move you towards a more robust service and avoid headaches about losing state. I may have missed something in your description that makes what I described above inadequate, though. If that’s the case, let me know what it is, can I can try and provide a better answer.

mjadczak

mjadczak

This will have the disadvantage of blocking the caller until a connection is established. However, if it’s needed, you can use this to your advantage by implementing a form of back-pressure—for the first X calls, handle with the internal queue, but if that gets too large, refuse to handle the calls (:noreply) and only reply to them once their request is processed so that they cannot submit more requests until then. Watch out for GenServer.call timeouts there though.

hpopp

hpopp

:queue is fine for things like this, and generally how I’d implement it. For anything more sophisticated I’d take a look at GenStage.

mjadczak

mjadczak

In that case, you could also use casts instead of calls.

idi527

idi527

Probably a bad idea, but you can use the process’s mailbox for queuing incoming massages. Just don’t handle_* them and wait for some condition (connection’s readiness in your case). I usually do it this way, but one day it will surely bite me.

Where Next?

Popular in Questions Top

quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

Other popular topics Top

lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New

We're in Beta

About us Mission Statement