Skip to main content

gwk_kernel/wire/
subscribe.rs

1//! Live subscriptions: the half of the protocol that arrives unasked.
2//!
3//! A subscription is a cursor and a loop. It reads forward from where the
4//! consumer last was, sends what it finds, and then waits for a reason to look
5//! again. Everything interesting is in what counts as a reason, and in what
6//! happens when the consumer stops keeping up.
7//!
8//! # Two wake-ups, because one of them is allowed to fail
9//!
10//! The append path already queues a `pg_notify` INSIDE its transaction, so a
11//! wake-up is delivered only if the append committed and never announces one
12//! that rolled back. That is the fast path and it is the normal path.
13//!
14//! It is not a guarantee. A notification can be lost — a dropped listener
15//! connection, a backend restart, a listener that had not reconnected yet — and
16//! PostgreSQL will not redeliver it. On a busy log that heals itself, because
17//! the next append wakes everyone anyway. On a log that goes quiet immediately
18//! after, it does not heal at all: the consumer waits for an append that has
19//! already happened, forever. So every subscription ALSO re-reads on
20//! [`SUBSCRIPTION_POLL_SECS`], which turns the worst case from unbounded into a
21//! number.
22//!
23//! The wake-up is a [`tokio::sync::watch`] rather than a notify, and that
24//! choice does real work: `changed()` is edge-triggered against a value the
25//! receiver has not yet observed, so an append landing BETWEEN a read and the
26//! wait is still pending when the wait begins. A primitive that only wakes
27//! current waiters would drop exactly that one, and it is the likeliest race
28//! here — the window is the width of a database round trip.
29//!
30//! # A slow consumer loses its subscription, not its connection
31//!
32//! Delivery is at-least-once and the cursor is what makes that safe: a consumer
33//! that reconnects from its last delivered sequence misses nothing and may see
34//! a repeat. When a consumer stops reading, the queue fills and the send blocks;
35//! after [`SLOW_CONSUMER_TIMEOUT_SECS`] the subscription gives up and says so
36//! with the cursor the consumer ACTUALLY received — not the one the kernel had
37//! reached — so resuming from it has no gap. The connection stays open and its
38//! other work carries on.
39//!
40//! "Actually received" is why a batch travels with a progress cell rather than
41//! alone. Queuing a batch is not delivering it: the write loop buffers up to
42//! `BATCH_QUEUE_DEPTH` of them, so the cursor the kernel has READ to can be
43//! thousands of events past the last frame that reached the socket — and a
44//! client resuming from that number would skip every event in between. The cell
45//! is written by the loop that owns the write half, after the frame is out, so
46//! the number this subscription closes with is one the client has seen. It can
47//! lag a frame that is being written as the timeout fires; lagging repeats an
48//! event, which at-least-once allows, where leading loses one.
49
50use std::sync::Arc;
51use std::sync::atomic::{AtomicU64, Ordering};
52use std::time::Duration;
53
54use gwk_domain::ids::{RequestId, Seq};
55use gwk_domain::port::EventStore;
56use gwk_domain::protocol::{
57    KernelErrorCode, SLOW_CONSUMER_TIMEOUT_SECS, SUBSCRIPTION_POLL_SECS, ServerControl,
58};
59use sqlx::PgPool;
60use sqlx::postgres::PgListener;
61use tokio::sync::{mpsc, watch};
62
63use super::serve::{Outgoing, fit_page};
64use crate::store::{EVENT_CHANNEL, PgEventStore};
65
66/// Events read per catch-up page.
67///
68/// A subscription that is far behind walks forward a page at a time rather than
69/// asking for everything at once; the byte cut in [`fit_page`] then trims that
70/// page to what one frame can carry.
71const SUBSCRIPTION_BATCH_ROWS: usize = 256;
72
73/// Turn database notifications into wake-ups, until the daemon stops.
74///
75/// The published value is a counter, not a watermark: subscriptions re-read
76/// from their own cursors anyway, so all this has to carry is "something
77/// changed". A counter is always different from the last one, which means a
78/// receiver can never mistake two appends for one.
79///
80/// A listener that cannot connect or that dies is not fatal, and deliberately
81/// so — it degrades a subscription from milliseconds to
82/// [`SUBSCRIPTION_POLL_SECS`], and a kernel that refused to serve at all
83/// because its notification channel was unavailable would be trading a slower
84/// stream for no stream.
85pub async fn watch_events(pool: PgPool, wake: Arc<watch::Sender<u64>>) {
86    let mut listener = match PgListener::connect_with(&pool).await {
87        Ok(listener) => listener,
88        Err(_) => return,
89    };
90    if listener.listen(EVENT_CHANNEL).await.is_err() {
91        return;
92    }
93    let mut tick = 0u64;
94    while listener.recv().await.is_ok() {
95        tick = tick.wrapping_add(1);
96        // `send_replace` rather than `send`: with no subscriptions on any
97        // connection there are no receivers, and a plain send would report that
98        // as an error on every single append.
99        wake.send_replace(tick);
100    }
101}
102
103/// Why a subscription stopped, when it stopped for a reason worth reporting.
104///
105/// The reason travels as a code and nothing more, because `StreamClosed` carries
106/// no message: what a client does about a stream that ended is resume from the
107/// cursor, and the two codes that reach it — a slow consumer and a failed read —
108/// are the whole of what it can branch on.
109enum Ended {
110    /// The consumer stopped reading and the queue stayed full.
111    SlowConsumer,
112    /// The log could not be read.
113    Storage,
114    /// The connection went away; nobody is left to tell.
115    Gone,
116}
117
118/// Deliver from `cursor` forward until the consumer stops keeping up or the
119/// connection ends.
120///
121/// `batches` is the queue the consumer drains and `responses` is the priority
122/// one, which is why a `StreamClosed` can still be delivered to a consumer
123/// whose batch queue is precisely the thing that is full.
124///
125/// `delivered` starts at the cursor this subscription was opened with, so a
126/// stream that ends having sent nothing reports where it began rather than the
127/// beginning of the log.
128pub(crate) async fn run(
129    log: Arc<PgEventStore>,
130    request_id: RequestId,
131    mut cursor: Option<Seq>,
132    batches: mpsc::Sender<Outgoing>,
133    responses: mpsc::Sender<ServerControl>,
134    mut wake: watch::Receiver<u64>,
135    delivered: Arc<AtomicU64>,
136) {
137    let ended = loop {
138        let events = match log.read_from(cursor, SUBSCRIPTION_BATCH_ROWS).await {
139            Ok(events) => events,
140            Err(_) => break Ended::Storage,
141        };
142        if events.is_empty() {
143            // Nothing to send: wait for a notification or for the poll interval
144            // to come round, whichever is first. `wake.changed()` was armed by
145            // not having been observed since the last loop, so an append that
146            // landed during the read above is already pending here.
147            tokio::select! {
148                changed = wake.changed() => {
149                    if changed.is_err() {
150                        // The listener task is gone. The poll still works, so
151                        // this is slower, not broken.
152                        tokio::time::sleep(Duration::from_secs(SUBSCRIPTION_POLL_SECS)).await;
153                    }
154                }
155                () = tokio::time::sleep(Duration::from_secs(SUBSCRIPTION_POLL_SECS)) => {}
156            }
157            continue;
158        }
159
160        // The page is cut to what a frame can carry; whatever is left is picked
161        // up on the next turn of the loop, immediately, because the cursor moved.
162        let (events, _cut) = match fit_page(events) {
163            Ok(page) => page,
164            Err(_) => break Ended::Storage,
165        };
166        // `fit_page` always keeps its first item, so a non-empty read cannot cut
167        // to nothing — and if it ever did, standing still with a cursor that
168        // never moves would be the worse failure.
169        let Some(last) = events.last().map(|e| e.global_sequence) else {
170            break Ended::Storage;
171        };
172
173        let batch = Outgoing {
174            control: ServerControl::EventBatch {
175                request_id: request_id.clone(),
176                events,
177                cursor: last,
178            },
179            delivered: Some((Arc::clone(&delivered), last.value())),
180        };
181        match tokio::time::timeout(
182            Duration::from_secs(SLOW_CONSUMER_TIMEOUT_SECS),
183            batches.send(batch),
184        )
185        .await
186        {
187            // Queued, which is what lets the READ cursor advance: a batch that
188            // never reached the queue is re-read rather than skipped. Whether it
189            // was DELIVERED is a different question, answered by the cell above
190            // once the frame is written.
191            Ok(Ok(())) => cursor = Some(last),
192            Ok(Err(_)) => break Ended::Gone,
193            Err(_) => break Ended::SlowConsumer,
194        }
195    };
196
197    let code = match ended {
198        // Nobody is left to tell, and the connection's own teardown is already
199        // the message.
200        Ended::Gone => return,
201        Ended::SlowConsumer => KernelErrorCode::SlowConsumer,
202        Ended::Storage => KernelErrorCode::Storage,
203    };
204    // Not `cursor` — that is how far the kernel READ, and on a slow consumer it
205    // is exactly the queue's depth ahead of the last frame that left. The cell
206    // is what the client has seen, and a resume from it is gap-free.
207    let last_cursor = match delivered.load(Ordering::Acquire) {
208        // The subscription opened at the beginning and never got a frame out.
209        0 => None,
210        seq => Some(Seq::new(seq)),
211    };
212    let _ = responses
213        .send(ServerControl::StreamClosed {
214            request_id,
215            code,
216            last_cursor,
217        })
218        .await;
219}