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
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
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
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
I’ve not used Rabbit, but I don’t see why you would open and close a connection repeatedly.
Popular in Questions
Other popular topics
Latest Phoenix Threads
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex









