Mor

Mor

Back-Pressure with Event Ordering - Architecture Suggestions

Overview

I am wondering if anyone has any suggestions or ideas regarding the best way to handle flow control in a real-time streaming app I am building. I apologize for the long post.

Generally, I believe I can solve at least some of my problems using a combination of 1 or more GenStage, worker pools, tasks, brokers/ready signaling. I have some other concerns such as GC and minimizing sends that I can save for responses.

Overall, I’m looking for a suggestion on the best way to organize processes for my pipeline.

Background

I have a dynamic graph of processors possibly on multiple BEAM nodes that perform computations. Event data is read from a ring buffer per-processor stored in ETS. Each event has a corresponding sequence number. Work is performed in order of sequence number and only if an event is interesting to a particular processor.

Output is sent downstream to a writer in the order it is processed - by sequence number. Each writer process inserts data into its own ring buffer and signals upstream whenever there is a given amount of minimum space in its ring buffer. This is a lot like GenStage except a bit more decoupled, optimized more for my specific use-cases, and supports load balancing/queueing across many different producers.

The output produced by computations can be 0 to n items in size, therefore the data is wrapped into a stream when possible. Data is batched in general - 1 or more batches per sequence number and per send to the writer. A sequence number is done when its output has been fully written to the destination ring buffer. This may or may not include ACKing back asynchronously depending on config (as well as journaling) after each item or after n items.

Goal

My overall goal is I want to make sure that there are always computation results in-flight to be delivered downstream to the writer. A single sequence number may produce huge amounts of items, 0 items, or only 1 item; therefore I cannot split on sequence number alone. In other words, solutions that deliver all results eagerly to a process mailbox like using Task.async_nolink without somehow splitting up things further won’t work.

Moreover, since the results of each sequence number are sent to a writer in order, I have a form of head of the line blocking. I experimented with delivering results out of order and re-ordering, but since it is hard to tell how many items might be produced, this both bloats memory/buffers and makes bookkeeping/rewinds much more tricky to the point where on average it is usually easier and often faster to just use head of the line blocking. The best I can do is deliver the results created by a single sequence number out of order which the stream already handles. Note that even though there is head of the line blocking, the system is otherwise hugely parallel in terms of many processes sending to writers - the ordering is a business requirement.

Pipeline Summary

Here’s a simplified version of the steps taken:

  1. Get the next sequence number
  2. Use the sequence number to lookup an event’s data by sequence number in ETS and check if it is interesting. If not interesting, publish the sequence number.
  3. Perform work on the event. If there is no output, publish the sequence number and move on to the next.
  4. If the writer has already signaled it is ready, send at most the amount of items the writer asked, otherwise, wait.
  5. Repeat step 4 until the batch produced by the current sequence number(s) is drained.
  6. Publish the sequence number once an ACK is received. If a NACK is received, respond accordingly.

The key here is sending as much as possible to the writer in a single batch, but not loading in too much into memory or being forced to wait on a potentially blocking computation. The data should ideally be ready for the writer at all times without realizing more than is needed. Simply put - be at least 1 output batch ahead of the writer when possible and dispatch realized results.

Ideally, whatever process is sending to the writer should have already received the ready signal before knowing it has new data so it may send ASAP without waiting for “ready” confirmation from the writer. The obvious exception is when the writer is actually backlogged.

Options

My basic components, some of which are optional include:

  1. Seq number producer - Usually a GenStage producer or GenServer that just sends an integer representing the next sequence number. It can also be used to filter out uninteresting events earlier in the pipeline.
  2. Seq number consumer - Performs or launches computation (via task supervisor or worker pool) and usually a GenStage consumer or consumer supervisor.
  3. Broker - Used for any kind of async notification of 2 parties to signal ready + some request data
  4. Writer - Performs the write to the destination buffer. May or may not be on the same node and typically would use a broker to signal ready + max event count, but I will omit that here for brevity.
  5. Dispatcher - GenServer process used to just peform sending work and receive writer ready notifications typically. Some options will be used to pull data from another process, other times receive data.

Here are some rough options using GenStage in whole or part that can be discussed more later:

seq producer -> seq consumer -> writer
seq producer -> seq consumer -> dispatcher -> writer
seq producer -> seq consumer <-> broker <-> dispatcher -> writer
seq producer -> seq consumer -> task <-> broker <-> dispatcher -> writer
seq producer -> seq consumer <-> broker <-> dispatch prod.-> dispatcher cons. -> writer
seq producer -> seq consumer -> task -> writer
seq producer -> seq producer consumer (worker) -> seq consumer -> writer 
seq producer -> worker(s) from pool of standby workers -> writer

Thoughts

Since this post is already too long, I’ll summarize some basic thoughts:

  • Use GenStage to signal upstream when to do work/emit results
  • Use a worker pool with 1 worker active or otherwise ensure there’s a sort of “send” mutex via message/ETS lookup.
  • Use a dispatcher so we have a process always ready to send to the writer and can handle the back/forth with the writer + bookkeeping like monitors.
  • Use ETS to shovel data to avoid send overhead. Seems redundant since destination is eventually a different ETS table. Plus side is it makes tracking progress easy + restart survival/resume. Already have a lot of ETS tables so want to avoid more.
  • Use intermediate processes/GenStages for the pre-fetch and/or compressing data using term_to_binary to cut down on mem size/send overhead potentially.
  • Use a broker to signal to parties when they are ready, and to add probes/dropping/request queueing to some degree if/when necessary. Ex: sbroker
  • Use Tasks/Pooled Workers that can be destroyed each run or after some given time/hibernated to deal with GC and avoid blocking if necessary.
  • Avoid polling
  • Avoid giving workers too much responsibility/stuff to do.
  • Try to send directly from what created the data without extra message passing.
  • Minimize number of processes when possible.

My current solution is essentially a more complicated version of the last one listed. The gist is that output is sent directly to the writer from the worker that is computing the results. In this case, I’m using GenServers/gen_statem to monitor workers I grab from a pool, sending each 1 or more sequence numbers (or alternatively I can fetch the event first from ETS). I’ve also experimented with having each worker itself be a GenStage to ensure there’s data in flight.

My other major alternative is one mainly using manual asking and multiple GenStages. My original design used a consumer supervisor, but I found this to be a bit obtuse with regard to restarts/worker control. Overall, a lot of the GenStage solutions mandate more message passing of output than I’d like unless I send to the writer from a task/worker directly.

At this point I’m a little unsure if there’s a better way. I suspect there are simpler or more performant ways. Suggestions? Thoughts? Thanks.

Where Next?

Popular in Questions Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
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
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
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
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
dotdotdotPaul
Okay, I’m having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I’m sure I’...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39297 209
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
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
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
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

We're in Beta

About us Mission Statement