enewbury

enewbury

Phoenix_pubsub_buffered - An in-memory downtime-tolerant pubsub adapter

Hi folks, looking for feedback on this new Phoenix pubsub adapter. It’s meant to be an in-memory lightweight substitute to using something like kafka or postgres WAL replication where it persists messages, and a set of cursors that subscribers can use to “catch up” on messages if they lose connection or get behind.

And by “lightweight” I mean that it doesn’t have full unlimited durability. Because it’s in-memory you get a constant-size ring buffer to hold messages. If a consuming node is disconnected from a producing node for so long that the read cursor is overwritten, when the node re-establishes a connection all subscribers on the consuming node will receive a special message

{:cursor_expired, publisher_node_name}

At that point, it is the application code’s responsibility to return to a valid state, by doing something like reloading state from a source of truth like a db or other node, etc.

Design

Producers are in charge of maintaining the set of cursors. This allows producers to “catch up” previously connected nodes, while newly connected nodes can create a new cursor and only receive messages going forward.

This brings up another edge case, if there is a network partition, and during that time, the producer restarts and loses all it’s state, including cursors. When the nodes reconnect, the consuming node will not know that it missed some messages during the outage since it will be seen as a “new node” to the producer and only get future messages.

This is a condition that this library is explicitly here to prevent. We want a guarantee that if I see message 3, that means I’ve also seen 2 and 1. There should never be gaps in messages. This allows us to use this library for streaming things like event_sourced messages where none can be missing. Subscribers can be “behind” or “eventually consistent” but they shouldn’t ever contain a state that never existed on the producer. I can’t remember what this property is called… so if someone can remind me (strict serializability?)

To solve this issue, consumers also keep a record of all the producers that have previously registered with. When they reconnect with a node they are previously registered with, they let the producer know they are registered. If the producer doesn’t have a record of that, it knows something is wrong and send the {:cursor_expired, producer_node_name} message so the consumer knows it needs to reset state.

Some future iteration concerns:

  • Even though the state is stored in a constant size ring buffer, the cursors are monotonically increasing integers. This was an optimization to not have to update all cursor values periodically, but I might look into that in the future. In practice, I believe integers don’t have a “max value” in elixir, and for modern use cases of elixir, we often deploy enough that this gets reset frequently anyway.

  • Currently I also don’t “clean-up” old sessions, so if you have a long running node, and many consuming nodes come and go over time, the number of cursors would continue to grow. In the future I may add a cleanup timer to remove cursors that are expired anway.

  • Could provide the option for producers to fsync their buffer to disk for applications that want to minimize “expired” situations due to producer crashes.

Links

Also if you have a better idea for a name, I’m open to suggestions!

Most Liked

enewbury

enewbury

Sure, our use case is basically hot-standbys of an in-memory statemachine. The source of truth is always written to the database, but reloading all state from DB is expensive and we want to “keep up” in real time with the leader node. However, if a follower gets too far behind, it’s trivial to just reload the source of truth from db and continue replicating from there. (When we actually do a failover, we have to block writes on the leader and send a flush message through so we know a follower is fully replicated). We were doing all of this previously with the postgres WAL, but we ran into a lot of headaches and limitations for our usecase.

Basically anything where a consumer of messages can’t tolerate “not knowing” that they’ve missed messages but they have the capability to “restart” from a known good state as long as they are notified that there was a problem.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Hey @enewbury this is super cool. Can you talk a bit about a use case or scenario where this sort of semi-persistent pubsub approach is the right balance?

enewbury

enewbury

Hmm, maybe not. This library just handles the fault-tolerance between distributed elixir nodes. Once it gets to a node, delivery to an external client over websocket is another thing. That’s where the adapter ends, and core phoenix pubsub takes over, which will “send” the message to all the individual subscriber processes on that local node. If one of those local processes is a channel that manages a websocket, and that socket is disconnected when a message comes in, I imagine it just drops it, or maybe the process dies along with the subscription. I doubt channels are buffering messages to websockets.
Anyone have any knowledge about that?

derek-zhou

derek-zhou

I see. For that to work, you would need to expose the cursor all the way to the client side for local storage and resume, or each message delivered to the client side would need to be acknowledged all the way back. Neither of them is easy.

Where Next?

Popular in Libraries Top

hpopp
After just over two years in development, this latest version of Pigeon is what I finally consider done in regards to my original vision ...
New
tompave
Hello there, I would like to share a feature toggles library (AKA feature flags) I’ve been working on. The main package is FunWithFlags...
New
josevalim
EDIT: since Ecto 3.0 final version is out, this post was amended to use the final versions in the instructions below. Hi everyone, We a...
New
benlime
I created a new library GitHub - benvp/ex_cva: Class Variance Authority for Elixir which aims to make it very easy to define different va...
New
Qqwy
Hello everyone, I wrote a small library today called MapDiff. It returns a map listing the (smallest amount of) changes to get from map...
New
mbuhot
EctoJob A transactional job queue built with Ecto, PostgreSQL and GenStage Available on Hex.pm: ecto_job | Hex Docs: API Reference — ec...
New
wmnnd
Hi there, for my project DBLSQD, I needed a file storage solution that is a bit more flexible than Arc. Because I thought others might f...
New
maltoe
Hello! Came here to announce ChromicPDF, a pet project PDF generator I’ve been working on for the past few months. Why another PDF gener...
New
versilov
Could not wait for the missing Elixir ML libraries to appear, so, I wrote one myself, taking https://github.com/sdwolfz/exlearn as a foun...
New
bluzky
You may know https://ui.shadcn.com/, a UI component library for React. I really love it’s design style and components. I’ve built some co...
381 12391 119
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
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
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
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
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
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

Sub Categories:

We're in Beta

About us Mission Statement