peppy

peppy

Need Help Building Simple Database Persistence: GenStage & Broadway?

Greetings,

I’m having a hell of a time trying to build a really simple script to collect chat messages and save them to the database… Unfortunately, I’m still too new to the language and things haven’t “clicked” yet in my mind. I recently bought this book to learn about genstage and broadway: Concurrent Data Processing in Elixir: Fast, Resilient Applications with OTP, GenStage, Flow, and Broadway by Svilen Gospodinov . While I did learn quite a bit more about the technologies, the examples provided there just didn’t apply to my specific case and I can’t find any examples to study. At the end of the book, he describes something about being able to set up persistence queue, but doesn’t actually provide the example.

What I need is a something that collects individual chat messages that come in the form of pre-formatted maps %{} and adds them to queue, or a big list. Example:

%{userid: 2, message: “hello test test test”, timestamp: …}
%{userid: 15, message: “how are you”, timestamp: …}
%{userid: 2, message: “foo bar”, timestamp: …}

If I have 1000 messages a minute, I would like to set up something scalable that will take batches of 10 message every 10 seconds and save them to mysql database using “insert_all”.

If the database is down or there is a connection error, I would like the script to continue to retry inserting the messages to the database without losing them, and keep trying for hours if needed, as our database has been known to go down for that long in rare cases.

I don’t know why I need to use RabbitMQ. I would like to just save these maps (messages) into a big simple list without having to set up a whole RabbitMQ server. Right now, I’m just trying to set up something really simple, and maybe when I become more knowledgeable, I could look into RabbitMQ again. I set up the custom transformation in the code below, as the book described, so I wouldn’t have to use RabbitMQ.

Here is what I have so far:

RoomChannel.ex: The function that sends the chat message map to the queue:

defmodule ExchatWeb.RoomChannel do
  use Phoenix.Channel
  alias ExchatWeb.Presence

  import Ecto.Query
  alias Exchat.Repo

  def handle_in("new_msg", %{"body" => body}, socket) do
    if (socket.assigns.is_anon == 0) do
      IO.inspect("user is logged in - send message")
      # This function sets the chat message to the database persistence script, formatted for database insert.
      :ok = ExchatWeb.TestProducer.save_message([%{message: body, userid: String.to_integer(socket.assigns.user_id), roomid: socket.topic, timestamp: DateTime.truncate(DateTime.utc_now(), :second)}])
      # sends the chat message to the chat app
      broadcast!(socket, "new_msg", %{body: body})
      {:noreply, socket}
    else
      IO.inspect("user is anonymous - do not send message")
      {:noreply, socket}
    end
  end
end

SaveTest.ex: Broadway Batcher:

defmodule ExchatWeb.TestSave do
  use Broadway
  require Logger

  def start_link(_args) do
    options = [
      name: ExchatWeb.TestSave,
      producer: [
        module: {ExchatWeb.TestProducer, []},
        transformer: {ExchatWeb.TestSave, :transform, []}
      ],
      processors: [
        default: [max_demand: 1, concurrency: 1]
      ],
      batchers: [
        default: [batch_size: 1, concurrency: 1, batch_timeout: 10_000]
      ]
    ]

    Broadway.start_link(__MODULE__, options)
  end

  def transform(event, _options) do
    %Broadway.Message{
      data: event,
      acknowledger: {ExchatWeb.TestSave, :pages, []}
    }
  end

  def ack(:pages, _successful, _failed) do
    :ok
  end

   def handle_message(_processor, message, _context) do
    if ExchatWeb.TestProducer.online?(message.data) do
      IO.inspect("handle message from Broadway")
      IO.inspect(message.data)
      Broadway.Message.put_batch_key(message, :default)
    else
      IO.inspect("handle message - failed")
      Broadway.Message.failed(message, "offline")
    end
  end

  def handle_batch(_batcher, [message], _batch_info, _context) do
    IO.inspect(message.data)
    IO.inspect("handle batch")
    IO.inspect([message])
    [message]
  end
end

TestProducer.ex: The producer used in the SaveTest.ex broadway batcher:

defmodule ExchatWeb.TestProducer do
  use GenStage
  require Logger

  def init(initial_state) do
    Logger.info("TestProducer init")
    {:producer, initial_state}
  end

  def handle_demand(demand, state) do
    Logger.info("TestProducer received demand for #{demand} pages")
    events = []
    {:noreply, events, state}
  end

  def save_message(pages) do
    ExchatWeb.TestSave
    |> Broadway.producer_names()
    |> List.first()
    |> GenStage.cast({:pages, pages})
  end

  def handle_cast({:pages, pages}, state) do
    {:noreply, pages, state}
  end

  def online?(_url) do
    # Pretend we are checking if the
    # service is online or not.
    # Select result randomly - (result will always be true for testing purposes).
    Enum.random([true, true, true])
  end
end

What I have here is Frankenstein code… taking examples from the book with parts that I won’t actually be using. Right now I’m trying to inspect and understand the flow of the messages, observe the queue, etc. I haven’t even gotten to the actual insert_all database call, nor the “retry” logic for database downtime yet. Right now, I’m just trying to figure out how to get the Producer queue to receive the individual chat messages (the maps %{...}) from the chat room and put them in a big list, or a queue. Then have the broadway batcher grab small batches of those messages out of the queue.

The first problem right now is that the producer receives the messages and it instantly ends up on the broadway script, it doesn’t wait 10 seconds, and it doesn’t come in multiple batches. It’s like there’s no queue or something.

To me, it seems like the solution and overall script has to be ridiculously simple, right? I’d greatly appreciate it if someone could provide a fully working example of this, and hopefully something will “click” in my head.

Most Liked

dimitarvp

dimitarvp

I believe in your case RabbitMQ is needed as a persistent message queue, i.e. if your DB server is down for hours you’ll just use RabbitMQ as an accumulated log of records to persist which is persisted itself.

You can do away with it and just accumulate messages in a plain GenServer message queue – or use :ets – but you are risking loss of all messages if your Elixir node goes down in the meantime because those methods are just in-memory queues. RabbitMQ can be made persistent.

Apart from that I am not even sure you need Broadway to be honest. I’d first try real hard to accept messages, queue them in RabbitMQ and have a supervised worker that periodically wakes up, pulls N messages and attempts to store them in the DB. If succeeded, you can ack the messages in RabbitMQ (which deletes them). If failed, you nack them (which keeps them in RabbitMQ). Sleep for X seconds, rinse and repeat. I likely don’t know your entire code and requirements but as I am describing it I’d easily fit the above in 3-4 files.

So IMO try take the more simple and “vanilla” route first?

cmo

cmo

Here, you are immediately putting all the pages you receive out for the processors to consume. When processes ask for demand in handle_demand, you’re giving them nothing. If you want to accrue jobs you need to queue the work in the producer’s state and keep track of the demand you haven’t served.

Your max demand and batch size is 1 so processors and batchers are going to take a message as soon as it arrives. You might want to play with those values.

I think of it like so:

  • producer keeps queue of work in it’s state
  • producer keeps track of how much demand has been requested by the processors but was unable to be served (remember the processors won’t keep polling for work after they request some and none is given, you have to push it to them once it arrives)
  • things send work to the producer or the producer requests work from somewhere
  • when jobs are enqueued to the producer, if demand has accrued, push that many messages out and reduce the accrued demand in the state, add the remaining jobs to the queue

If your database is down for hours you might end up with a lot of messages getting dropped, unless you use a producer that will persist them to disk for you, e.g. RabbitMQ.

What made it all click for me was building the pipeline in that book with a Logger/IO.puts in every function and watching it go round and round.

bartblast

bartblast

Creator of Hologram

If you have only ~1000 messages per minute and you want to make sure that all the messages are persisted even if the DB goes down from time to time, and if you don’t want to setup your own RabbitMQ node, then use AWS SQS - it’s trivially simple and cheap in such cases. Then you would use Broadway for pulling the messages from SQS and inserting them into the DB if it is operational.

Last Post!

cmo

cmo

I’ve not used Rabbit, but I don’t see why you would open and close a connection repeatedly.

Where Next?

Popular in Questions Top

vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
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
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
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
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

Other popular topics Top

New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54921 245
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
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New

We're in Beta

About us Mission Statement