Skip to main content

Module subscribe

Module subscribe 

Source
Expand description

Live subscriptions: the half of the protocol that arrives unasked.

A subscription is a cursor and a loop. It reads forward from where the consumer last was, sends what it finds, and then waits for a reason to look again. Everything interesting is in what counts as a reason, and in what happens when the consumer stops keeping up.

§Two wake-ups, because one of them is allowed to fail

The append path already queues a pg_notify INSIDE its transaction, so a wake-up is delivered only if the append committed and never announces one that rolled back. That is the fast path and it is the normal path.

It is not a guarantee. A notification can be lost — a dropped listener connection, a backend restart, a listener that had not reconnected yet — and PostgreSQL will not redeliver it. On a busy log that heals itself, because the next append wakes everyone anyway. On a log that goes quiet immediately after, it does not heal at all: the consumer waits for an append that has already happened, forever. So every subscription ALSO re-reads on SUBSCRIPTION_POLL_SECS, which turns the worst case from unbounded into a number.

The wake-up is a tokio::sync::watch rather than a notify, and that choice does real work: changed() is edge-triggered against a value the receiver has not yet observed, so an append landing BETWEEN a read and the wait is still pending when the wait begins. A primitive that only wakes current waiters would drop exactly that one, and it is the likeliest race here — the window is the width of a database round trip.

§A slow consumer loses its subscription, not its connection

Delivery is at-least-once and the cursor is what makes that safe: a consumer that reconnects from its last delivered sequence misses nothing and may see a repeat. When a consumer stops reading, the queue fills and the send blocks; after SLOW_CONSUMER_TIMEOUT_SECS the subscription gives up and says so with the cursor the consumer ACTUALLY received — not the one the kernel had reached — so resuming from it has no gap. The connection stays open and its other work carries on.

“Actually received” is why a batch travels with a progress cell rather than alone. Queuing a batch is not delivering it: the write loop buffers up to BATCH_QUEUE_DEPTH of them, so the cursor the kernel has READ to can be thousands of events past the last frame that reached the socket — and a client resuming from that number would skip every event in between. The cell is written by the loop that owns the write half, after the frame is out, so the number this subscription closes with is one the client has seen. It can lag a frame that is being written as the timeout fires; lagging repeats an event, which at-least-once allows, where leading loses one.

Functions§

watch_events
Turn database notifications into wake-ups, until the daemon stops.