How to listen to database changes using Postgres triggers in elixir(peterullrich.com) |
How to listen to database changes using Postgres triggers in elixir(peterullrich.com) |
This has been a really elegant and low-complexity way to get distributed pubsub without the complexity of running a distributed erlang cluster (which seems a lil bit painful in a K8S+Continuous Deploy world)
There -are- some big downsides to be aware of though.
1. You can't use PgBouncer w/ LISTEN/NOTIFY. This has been really painful because of the high memory overhead of a pgsql connection + elixir keeping a pool of open pgsql connections. The tried and true method of scaling here is to just use PgBouncer. We've kicked the can on this by vastly over-provisioning our pg instance, but this has cost $10s of thousands on the cloud. Of course, it's solvable (dedicated non-pgbouncer connection pool just for LISTEN/NOTIFY, for example), but painful to unwind.
2. The payload has a fixed size limit (8KB, IIRC). This has bitten us a few times!
Even though I really like pg_notify, I think that if I were starting over, I'd probably just use Redis Pub/Sub to accomplish the same thing. Tad bit more complex if you're not already running Redis, but without the downsides. (Of course, w/ Redis, you don't get the elegance of firing a notification via a pg trigger)
function software_listen(channel, callback):
if not channel_listened(channel):
sql("LISTEN " + channel)
listeners[channel].append(callback)
function on_message(channel, data): for listener in listeners[channel]
listener(channel, data)
function unlisten(channel, listener): listeners[channel].remove(listener)
if len(listeners[channel]) == 0:
sql("UNLISTEN " + channel)
Here's the actual go implementation we use:https://gist.github.com/ColinChartier/59633c1006407478168b52...
If discovering nodes is difficult in your env, try using a listen/notify libcluster strategy:
https://github.com/supabase/supavisor/blob/main/lib/cluster/...
Background/announcement: https://supabase.com/blog/supabase-realtime-multiplayer-gene...
And we didn't want people schemas polluted with triggers.
But also we use Erlang distribution and Phoenix.PubSub because with a global network clients connected to the same node in the same region will get normal Broadcast messages to each other faster. If we ran them through Postgres or Redis the added latency wouldn't work for a global thing.
It reminds me of a very similar post I put out in 2018 https://by.ben.church/Get-notified-of-user-signups-and-plan-...
But I think Peter did a much better job going through the mechanics and providing a more modernized example.
For those that are curious there are pitfalls (that can be worked around)
1. If your DB goes down you may loose messages
2. If you have multiple backends behind a load balancer you may trigger additional events
3. There is a limit to the payload size you can send through these triggers
But for those that want to try this approach I do have a library here that does wraps everything Peter layed out: https://github.com/bnchrch/postgrex_pubsub
Also if you want something even better I recommend WalEx https://github.com/cpursley/walex
Which is based on WAL logs and doesnt have the same limitations.
> PERFORM pg_notify('appointments_canceled_changed', payload);
> Be aware that this listener can easily become a bottleneck if you have lots of messages. If you can’t handle the messages quickly enough, the message queue will fill up and crash your application. If you’re worried about this case, you could create one listener per channel or use a PartitionSupervisor to start more handlers and spread out the work.
Why not insert into an events table instead of pg_notify? That way the events are recorded within the database itself, can be processed by any component, the state of processing can be saved in the table so even if the component dies, it can resume (and can even fan out the actual processing to workers). Further, you have the record of all events alongwith the flexibility of interacting with the event information with SQL and with partitioning, you can have a clean way to manage performance + ability to easily archive past/processed events.
"Debit account $100" is persistent.
"Account balanced changed" is ephermeral.
Storing ephemeral events would be a large amount of overhead; pg_notify is far faster as it does not write to storage or WAL.
Sonification for monitoring and debugging distributed systems
We're already tracking changes for the purposes of time travel queries and other auditing purposes using Temporal Tables (SQL:2011 feature). I'm thinking a cron job triggering a lambda every minute should be sufficient to read from the history tables and publish out change data events over a bus.
Anyone see any problems with this approach?
It looks like as long as transactions are not taking excessive time to complete, that temporal tables will be sufficient, since the history tables get marked with the transaction begin times. I'll use a sliding window approach and dedupe.
EDIT: I also found this https://learn.microsoft.com/en-us/sql/relational-databases/s... which seems like it's supported on Express and Web versions and should be a fairly robust solution.
CDC has 20-40 second delay for mssql which makes it unusable for many purposes.
FWIW, we're (estuary.dev) an open-source and fully managed tool for building CDC pipelines out of Postgres from the WAL.
High notes for inserts, low rumbles for reads or something. That could be pretty interesting actually.
Oban jobs run almost instantly when they are queued so there's no perceptible difference in speed, but you get all the benefits of resilience that Oban provides, so your webapp connection can go down, such as during a deploy, and still catch up reliably.
It's also handy to be able use the oban_jobs table for debugging purposes.
If you must care about bit-level accurate replication, catching gaps after the replication completes is we concluded after a year of chasing that chimera a deep rabbit hole with an event horizon that constantly recedes into the future. If you can tolerate some replication errors and can tolerate not knowing where they happen without a lot of investigation, then CDC works great.
CDC gets us close, but I'm still looking for someone who is working upon covering the edge cases that redo logs alone do not address.
Yeah I was gonna say, there are probably ways to use the PostgreSQL Write-Ahead Log (WAL) to stay up to date with every change, without having triggers. This CDC you mention sounds similar to that.
You should use embedded mode if you do not require fault-tolerance and can miss updates. Otherwise, don't. Regardless of scale.
CDC is such a great concept and is so little used unfortunately
One other benefit is they capture all the changes to the underlying data, not just the net changes.
It’s important to realize though that CDC records change information but isn’t a mechanism to move it anywhere. You would still have to devise a means to move the data to another system.
Debezium is a data movement tool that uses CDC for the underlying tracking.
https://github.com/vippsas/mssql-changefeed
V1 requires to run a sweeper procedure in the background, but an upcoming v2 version does without the sweep loop. Unfortunately too fresh for readme to be updated, but relevant lines in tests to show it off:
https://github.com/vippsas/mssql-changefeed/blob/v1-lazy/go/...
https://github.com/vippsas/mssql-changefeed/blob/v1-lazy/go/...
https://github.com/vippsas/mssql-changefeed/blob/v1-lazy/go/...
Library itself:
https://github.com/vippsas/mssql-changefeed/blob/v1-lazy/mig...
https://hexdocs.pm/postgrex/Postgrex.ReplicationConnection.h...
Here's a great talk on Postgrex Relication: