PostgreSQL NOTIFY to RabbitMQ Bridge & Event Outbox
Overview
Many events in this system originate at the database level. When a user taps a campaign link, for example, a row is inserted into tracking_log and a Postgres trigger fires pg_notify.
The pg-listener profile is the process that holds a dedicated connection open, LISTENs on 5 channels, and converts every notification into a RabbitMQ message for the trigger worker to process.
Because pg_notify is fire-and-forget — events are lost if the connection drops — the system also keeps an event_outbox table as a durable backup. The pg-listener catches up after each reconnect, sweeps stalled rows, and periodically cleans out old ones.
Business Flow
Listening and publishing
-
Open a dedicated connection rather than using the shared pool, because
LISTENmust stay bound to a single connection for its entire lifetime. -
LISTENon all 5 channels, each mapped to a destination queue:pg_notify channel RabbitMQ queue attribute_changedattribute_changefriend_track_event_insertedfriend_track_event_triggerform_submittedform_submitted_triggercampaign_clickcampaign_click_triggerbooking_eventbooking_event_trigger -
On each notification, unmarshal the payload and re-marshal it with
encoding/jsonso the bytes match V8's canonical form, then publish it to the mapped queue verbatim, with no envelope wrapper. -
If the connection drops, reconnect with exponential backoff of
min(2^n seconds, 30s)for up to 100 attempts, then callos.Exit(1)so the orchestrator restarts the pod.
Managing the event outbox
- Catch-up — after a successful reconnect, fetch rows from
event_outboxcreated after the disconnect timestamp usingFOR UPDATE SKIP LOCKEDand republish them, so no event is lost during downtime. - Sweep — every 5 minutes, pick up rows that have been pending for more than 30 seconds without being delivered.
- Cleanup — every hour, delete rows older than 24 hours.
Health checks
- Registers a readiness probe named
pg-listener; if the LISTEN connection drops,/readyzfails immediately. - Exposes
GET /healthon the health port (PG_LISTENER_HEALTH_PORT, default 6010), returningstatus,pg.connected,pg.disconnectedAt, andtimestamp.
Key Files & Functions
internal/pglistener/service.goNew(cfg, pub, log)— constructs the bridge; the channel-to-queue mapping comes fromconfig.Queues.*rather than being hardcodedRun(ctx),listenLoop(),waitLoop(),handleNotification()catchupFromOutbox(),sweepOutbox(),cleanupOutbox()backoff(),IsConnected(),DisconnectedAt()
cmd/worker/main.go—runPgListener(), which registers the readiness probe and the/healthhandler- Published queues:
attribute_change,friend_track_event_trigger,form_submitted_trigger,campaign_click_trigger, andbooking_event_trigger
Connections to Other Services
- PostgreSQL — a dedicated
pgx.Connfor LISTEN, plus theevent_outboxtable accessed through SQL copied verbatim from the original implementation - Database-side Postgres triggers — for example
tracking_log_campaign_click_notify, which firespg_notifyon thecampaign_clickchannel and inserts intoevent_outboxin the same statement (see the database project documentation for the schema) - RabbitMQ — this profile only publishes; it never consumes
- Downstream consumers — all 5 queues are consumed by the
trigger-workerprofile, exceptbooking_event_trigger, which is handled bycron-scheduler