Skip to main content

gwk_kernel/wire/
serve.rs

1//! One connection, from hello to hangup — and the accept loop that owns them.
2//!
3//! What is served here is the request/response surface: the readiness answers,
4//! the one mutation path, and the reads — a projection by id, a projection page,
5//! and a page of the log. Every one of them is a question with an answer, which
6//! is what makes them one layer: a request goes out, a response comes back on
7//! the same `request_id`, and nothing arrives that was not asked for.
8//!
9//! Subscriptions are the other half, and they are why a connection is two loops
10//! instead of one. A batch is a frame the client did not individually request,
11//! so it cannot be written by the code that answers requests: one loop owns the
12//! write half and takes work from two queues, responses before batches. What a
13//! subscription then DOES lives in [`subscribe`](super::subscribe) — this layer
14//! admits one, bounds how many a connection may hold, and writes what they
15//! produce.
16//!
17//! Blob transfer is here too, and it is thinner than it looks: the port already
18//! owns staging, digest verification, dedup, and encryption, so what this layer
19//! adds is the wire's own concerns — base64 in and out, a bound on a chunk, and
20//! a clamp on a read.
21//!
22//! Pages are cut by BYTES, not rows. The frame limit is the real bound and a row
23//! count cannot respect it — see [`PAGE_BYTE_BUDGET`].
24//!
25//! No wildcard arm anywhere: every request is matched by name, so the compiler
26//! has to notice when the protocol grows one.
27
28use std::sync::Arc;
29
30use base64::prelude::{BASE64_STANDARD, Engine as _};
31use gwk_domain::blob::BLOB_CHUNK_BYTES;
32use gwk_domain::ids::{ByteCount, EventCount, EventId, RequestId, Seq, WriterEpoch};
33use gwk_domain::port::{BlobError, BlobStore, EventStore, MAX_READ_LIMIT};
34use gwk_domain::protocol::{
35    CONNECTION_EGRESS_BYTES_PER_WINDOW, CONNECTION_INGRESS_BYTES_PER_WINDOW, CONTRACT_VERSION,
36    FRAME_BODY_MAX_BYTES, FrameKind, KernelErrorCode, KernelRequest, KernelResult,
37    MAX_SUBSCRIPTIONS_PER_CONNECTION, ProjectionKind, ProjectionRecord, ServerControl,
38};
39use sqlx::Row;
40use tokio::io::{AsyncRead, AsyncWrite};
41use tokio::net::UnixStream;
42use tokio::sync::{mpsc, watch};
43
44use super::frame::{Budget, Incoming, read_frame, write_frame};
45use super::hello::{self, Readiness};
46use super::listen::Listener;
47use super::{WireError, strict, subscribe};
48use crate::blob::store::PgBlobStore;
49use crate::epoch::{
50    GENESIS_EVENT_TYPE, KERNEL_AGGREGATE, KERNEL_SINGLETON, epoch_of, is_public_revision,
51};
52use crate::error::{KernelError, Result};
53use crate::store::PgEventStore;
54
55/// How long shutdown waits for connections that are mid-request.
56pub const DRAIN_TIMEOUT_SECS: u64 = 30;
57
58/// Ceiling on one page's serialized items, cursor continuation aside.
59///
60/// A row count cannot bound a page. An event or an ingested record carries up
61/// to `INLINE_PAYLOAD_MAX_BYTES` — 64 KiB — of payload, so sixty-four of them
62/// already exceed [`FRAME_BODY_MAX_BYTES`], and the response would fail at the
63/// WRITE rather than the request: a fatal framing error, connection closed,
64/// over a request that was perfectly well formed. Pages are therefore cut by
65/// bytes and the cursor says where. Half the frame leaves room for the response
66/// envelope and for re-serialization to land slightly wider than the estimate.
67const PAGE_BYTE_BUDGET: usize = (FRAME_BODY_MAX_BYTES as usize) / 2;
68
69/// Rows fetched for a projection page the request gave no limit for.
70const DEFAULT_PAGE_ROWS: u32 = 256;
71
72/// Batches the writer may hold for a client that has stopped reading.
73///
74/// One per subscription the connection is allowed, so a batch that is ready
75/// never waits behind another subscription's batch. What it can still wait on is
76/// the consumer, which is the only thing the slow-consumer timeout is meant to
77/// be measuring.
78const BATCH_QUEUE_DEPTH: usize = MAX_SUBSCRIPTIONS_PER_CONNECTION;
79
80/// Responses the writer may hold. One, because the request loop is serial: it
81/// reads a request, answers it, and does not read the next until that answer is
82/// queued, so a deeper queue would buffer responses that cannot exist.
83const RESPONSE_QUEUE_DEPTH: usize = 1;
84
85/// One frame on its way out, and what its departure proves.
86///
87/// A response proves nothing beyond itself, so `delivered` is `None` for one. A
88/// subscription's batch carries the cell its stream reports on closing plus the
89/// sequence to publish there — because a batch sitting in the queue has not
90/// reached the client, and only the loop that owns the write half can say when
91/// one has. See [`subscribe`](super::subscribe) for why that distinction is the
92/// difference between a gap-free resume and a silently skipped page.
93pub(crate) struct Outgoing {
94    pub(crate) control: ServerControl,
95    pub(crate) delivered: Option<(Arc<std::sync::atomic::AtomicU64>, u64)>,
96}
97
98/// Cut a page down to what one frame can carry, and say whether it was cut.
99///
100/// The first item always survives: a page that returned nothing because its
101/// leading item was large would make the cursor stand still and the client loop
102/// forever. One item cannot overflow a frame on its own — the payload bound is
103/// a small fraction of it — so admitting it unconditionally is safe.
104///
105/// ponytail: serializes each item once here and again in the response frame.
106/// Measuring without a second pass means threading a counting writer through
107/// the encoder; worth doing if pages ever show up in a profile, not before.
108pub(crate) fn fit_page<T: serde::Serialize>(items: Vec<T>) -> Result<(Vec<T>, bool)> {
109    let mut kept: Vec<T> = Vec::with_capacity(items.len());
110    let mut bytes = 0usize;
111    for item in items {
112        bytes += serde_json::to_vec(&item)
113            .map_err(|e| KernelError::Config(format!("measure a page item: {e}")))?
114            .len();
115        if !kept.is_empty() && bytes > PAGE_BYTE_BUDGET {
116            return Ok((kept, true));
117        }
118        kept.push(item);
119    }
120    Ok((kept, false))
121}
122
123/// The value a page's next cursor continues from, read out of the last record
124/// it delivered.
125///
126/// `key` is the column the query ordered by, carried here from the same table
127/// that holds the SQL — so the cursor a client gets back is by construction the
128/// value the next page's `>` compares against. Read off the record rather than
129/// selected as a second column, which keeps the read query's record expression
130/// byte-identical to the one the checkpoint hashes.
131fn cursor_key(record: &ProjectionRecord, key: &str) -> Result<String> {
132    let tag = record.kind().as_str();
133    let json = serde_json::to_value(record)
134        .map_err(|e| KernelError::Config(format!("serialize a {tag} record: {e}")))?;
135    json.get(tag)
136        .and_then(|body| body.get(key))
137        .and_then(serde_json::Value::as_str)
138        .map(str::to_owned)
139        .ok_or_else(|| KernelError::Config(format!("a {tag} record has no {key} to page from")))
140}
141
142/// What a connection needs to answer the sealed surface.
143pub struct Daemon {
144    /// Shared rather than owned because a subscription outlives the request that
145    /// opened it and reads the log from its own task.
146    store: Arc<PgEventStore>,
147    public_revision: String,
148    writer_epoch: WriterEpoch,
149    /// Publishes "the log grew" to every live subscription. Held behind an `Arc`
150    /// because the listener that feeds it lives in a task of its own.
151    wake: Arc<watch::Sender<u64>>,
152}
153
154impl Daemon {
155    /// `public_revision` is the build's own 40-hex revision, which `status`
156    /// reports so a client can compare a running daemon against the one genesis
157    /// recorded. It is required rather than defaulted: a daemon that reported a
158    /// placeholder would make that comparison answer "same" for two different
159    /// builds.
160    ///
161    /// The writer epoch is READ OFF the store rather than passed in, because
162    /// there is exactly one way to obtain one — `claim_epoch`, which BUMPS the
163    /// durable counter — and the store already did it when it opened. A caller
164    /// handed this as a parameter would have had to claim a second time, and a
165    /// second claim is not a second opinion about the epoch: it supersedes the
166    /// first, so the store this daemon is about to serve from would be fenced
167    /// out of its own log by the daemon in front of it.
168    /// A blob store is likewise required rather than optional. A store without
169    /// one is a real state — `admin init` and the certifier drive the log with no
170    /// filesystem at all — but a SERVING kernel in that state would answer every
171    /// blob request with a refusal that says nothing about the request, and the
172    /// operator would find out from a client rather than from a start-up that
173    /// failed.
174    pub fn new(store: PgEventStore, public_revision: String) -> Result<Self> {
175        if !is_public_revision(&public_revision) {
176            return Err(KernelError::Config(format!(
177                "public revision {public_revision:?} is not a full 40-hex revision"
178            )));
179        }
180        if store.blobs().is_none() {
181            return Err(KernelError::Config(
182                "a serving daemon needs a blob store: every payload too large to inline lives \
183                 there, and half a protocol is not a kernel"
184                    .to_owned(),
185            ));
186        }
187        let writer_epoch = WriterEpoch::new(store.boot_epoch().max(0) as u64);
188        // The initial receiver is dropped on purpose. A watch with no receivers
189        // is a normal state — a daemon nobody has subscribed on — and
190        // `send_replace` in the listener treats it as one.
191        let (wake, _) = watch::channel(0u64);
192        Ok(Self {
193            store: Arc::new(store),
194            public_revision,
195            writer_epoch,
196            wake: Arc::new(wake),
197        })
198    }
199
200    /// Start the listener that turns database notifications into wake-ups.
201    ///
202    /// Separate from [`Daemon::new`] because it spawns, and a constructor that
203    /// needed a running runtime to exist would be a trap for any caller that
204    /// builds a daemon before starting one. A daemon that never calls this still
205    /// serves subscriptions correctly — every one of them re-reads on
206    /// `SUBSCRIPTION_POLL_SECS` regardless — at poll latency instead of
207    /// notification latency.
208    pub fn notify_on_append(&self) {
209        tokio::spawn(subscribe::watch_events(
210            self.store.pool().clone(),
211            Arc::clone(&self.wake),
212        ));
213    }
214
215    /// The blob store, whose presence [`Daemon::new`] has already established.
216    fn blobs(&self) -> &PgBlobStore {
217        self.store
218            .blobs()
219            .expect("a daemon cannot be constructed without a blob store")
220    }
221
222    /// Sealed state and watermark, as the `HelloAck` reports them.
223    async fn readiness(&self) -> Result<Readiness> {
224        let mut conn = self.connection().await?;
225        let epoch = epoch_of(&mut conn)
226            .await
227            .map_err(|e| KernelError::Config(format!("read the epoch: {e}")))?;
228        let watermark = self
229            .store
230            .watermark()
231            .await
232            .map_err(|e| KernelError::Config(format!("read the watermark: {e}")))?;
233        Ok(Readiness {
234            sealed: epoch == crate::epoch::Epoch::Sealed,
235            watermark,
236        })
237    }
238
239    async fn connection(&self) -> Result<sqlx::pool::PoolConnection<sqlx::Postgres>> {
240        self.store
241            .pool()
242            .acquire()
243            .await
244            .map_err(|e| KernelError::Config(format!("acquire a connection: {e}")))
245    }
246
247    /// Answer one request, or say why not. A refusal is a `KernelResult`, never
248    /// an error out of band — the connection stays open and the client gets a
249    /// value it can branch on.
250    ///
251    /// `subs` is the connection's own state, threaded in because exactly one
252    /// request needs it: a subscription is the only answer that outlives the
253    /// response carrying it.
254    async fn answer(
255        &self,
256        request_id: &RequestId,
257        request: &KernelRequest,
258        subs: &mut Subscriptions<'_>,
259    ) -> KernelResult {
260        match self.try_answer(request_id, request, subs).await {
261            Ok(result) => result,
262            Err(e) => KernelResult::Error {
263                code: KernelErrorCode::Storage,
264                message: e.to_string(),
265                detail: None,
266            },
267        }
268    }
269
270    async fn try_answer(
271        &self,
272        request_id: &RequestId,
273        request: &KernelRequest,
274        subs: &mut Subscriptions<'_>,
275    ) -> Result<KernelResult> {
276        let readiness = self.readiness().await?;
277        Ok(match request {
278            // Liveness. A sealed kernel is READY — it is serving, it simply
279            // admits no business command yet, and conflating the two would make
280            // a health check fail every deployment between genesis and cutover.
281            KernelRequest::Health {} => KernelResult::Health {
282                ready: true,
283                sealed: readiness.sealed,
284            },
285            KernelRequest::Status {} => KernelResult::Status {
286                sealed: readiness.sealed,
287                watermark: readiness.watermark,
288                writer_epoch: self.writer_epoch,
289                contract_version: CONTRACT_VERSION,
290                public_revision: self.public_revision.clone(),
291            },
292            KernelRequest::Watermark {} => KernelResult::Watermark {
293                watermark: readiness.watermark,
294            },
295            KernelRequest::VerifySealed {} => self.verify_sealed(readiness.sealed).await?,
296
297            // Forwarded, not gated. `submit` reads the epoch inside its own
298            // transaction and under the writer lock, so the sealed allowlist is
299            // checked against the state the append will actually commit
300            // against. A second check out here would be read at a different
301            // instant than the one that matters — it could only ever refuse a
302            // command the transaction would have allowed, or wave one through
303            // that it then refuses anyway.
304            KernelRequest::SubmitCommand { envelope } => self.store.submit(envelope).await,
305
306            KernelRequest::GetProjection { projection, id } => {
307                // One row is a one-row page: the same query, asked for an exact
308                // key instead of a cursor.
309                let (mut records, _, _) =
310                    self.projection_page(*projection, None, Some(id), 1).await?;
311                match records.pop() {
312                    Some(record) => KernelResult::Projection { record },
313                    // Absent is NOT an error condition — it is an answer, and
314                    // one a caller routinely branches on.
315                    None => KernelResult::Error {
316                        code: KernelErrorCode::NotFound,
317                        message: format!("no {} with id {id:?}", projection.as_str()),
318                        detail: None,
319                    },
320                }
321            }
322
323            KernelRequest::ListProjection {
324                projection,
325                cursor,
326                limit,
327            } => {
328                // Clamped, never refused — the same rule the event read
329                // documents, so a client that asks for everything gets a page
330                // and a cursor rather than a rejection it has to special-case.
331                let rows = limit
332                    .unwrap_or(DEFAULT_PAGE_ROWS)
333                    .clamp(1, MAX_READ_LIMIT as u32);
334                let (records, cut, key) = self
335                    .projection_page(*projection, cursor.as_deref(), None, rows)
336                    .await?;
337                // A page that filled either bound may have more behind it. A
338                // full page that happened to end the table costs the client one
339                // extra empty page; claiming the end early would cost it rows.
340                let exhausted = !cut && (records.len() as u32) < rows;
341                let next_cursor = if exhausted {
342                    None
343                } else {
344                    records
345                        .last()
346                        .map(|record| cursor_key(record, key))
347                        .transpose()?
348                };
349                KernelResult::ProjectionPage {
350                    records,
351                    next_cursor,
352                }
353            }
354
355            KernelRequest::ReadEvents { cursor, limit } => {
356                let events = self
357                    .store
358                    .read_from(*cursor, *limit as usize)
359                    .await
360                    .map_err(|e| KernelError::Config(format!("read events: {e}")))?;
361                // `read_from` bounds the ROW count; this bounds the bytes, which
362                // is the bound a frame actually has.
363                let (events, _) = fit_page(events)?;
364                KernelResult::Events {
365                    // The last sequence actually delivered, so a client that
366                    // resumes from it resumes from what it received rather than
367                    // from what the database was asked for.
368                    cursor: events.last().map(|e| e.global_sequence),
369                    watermark: readiness.watermark,
370                    events,
371                }
372            }
373
374            // Admitted here, started by the request loop — see
375            // [`Subscriptions::admit`] for why the two are separate steps.
376            KernelRequest::SubscribeEvents { cursor } => subs.admit(request_id, *cursor),
377
378            // The blob half. Staging, ordering, the declared-size budget, digest
379            // verification, dedup, and encryption are all the port's; what is
380            // added here is base64, one bound, and one clamp.
381            KernelRequest::BlobBegin {
382                media_type,
383                byte_size,
384            } => match self.blobs().begin(media_type.clone(), *byte_size).await {
385                Ok(upload_id) => KernelResult::BlobBegun { upload_id },
386                Err(e) => blob_refusal(&e),
387            },
388
389            KernelRequest::BlobChunk {
390                upload_id,
391                sequence,
392                data_base64,
393            } => self.blob_chunk(upload_id, *sequence, data_base64).await,
394
395            KernelRequest::BlobCommit { upload_id, address } => {
396                match self
397                    .blobs()
398                    .commit(upload_id.clone(), address.clone())
399                    .await
400                {
401                    Ok((descriptor, deduplicated)) => KernelResult::BlobCommitted {
402                        descriptor,
403                        deduplicated,
404                    },
405                    Err(e) => blob_refusal(&e),
406                }
407            }
408
409            KernelRequest::BlobAbort { upload_id } => {
410                match self.blobs().abort(upload_id.clone()).await {
411                    Ok(()) => KernelResult::BlobAborted {
412                        upload_id: upload_id.clone(),
413                    },
414                    Err(e) => blob_refusal(&e),
415                }
416            }
417
418            KernelRequest::BlobRead {
419                address,
420                offset,
421                length,
422            } => {
423                // Clamped, not refused — the same rule the log and the
424                // projections follow. One chunk is the unit whose base64
425                // expansion is guaranteed to fit a frame, and a client that
426                // asked for a whole blob gets its head plus the offset to
427                // continue from rather than a rejection to special-case.
428                let length = ByteCount::new(length.value().min(BLOB_CHUNK_BYTES as u64));
429                match self.blobs().read(address, *offset, length).await {
430                    Ok(bytes) => KernelResult::BlobBytes {
431                        address: address.clone(),
432                        // Echoed, so a client reading a blob in pieces never has
433                        // to trust its own arithmetic about where this one began.
434                        offset: *offset,
435                        data_base64: BASE64_STANDARD.encode(&bytes),
436                    },
437                    Err(e) => blob_refusal(&e),
438                }
439            }
440
441            KernelRequest::BlobStat { address } => match self.blobs().stat(address).await {
442                Ok(Some(descriptor)) => KernelResult::BlobStat { descriptor },
443                // Absent is an ANSWER, exactly as it is for a projection. A
444                // blob that was SHREDDED is not this case — the port answers
445                // that as tombstoned, which is a different fact.
446                Ok(None) => KernelResult::Error {
447                    code: KernelErrorCode::NotFound,
448                    message: format!("no blob at {address}"),
449                    detail: None,
450                },
451                Err(e) => blob_refusal(&e),
452            },
453        })
454    }
455
456    /// One staged chunk, decoded off the wire.
457    async fn blob_chunk(
458        &self,
459        upload_id: &gwk_domain::ids::BlobUploadId,
460        sequence: u32,
461        data_base64: &str,
462    ) -> KernelResult {
463        // Decoded here rather than deeper in: base64 is a WIRE encoding and the
464        // port takes bytes. A bad encoding is the client's mistake, not the
465        // upload's, so it refuses this request and leaves the upload usable.
466        let chunk = match BASE64_STANDARD.decode(data_base64) {
467            Ok(chunk) => chunk,
468            Err(e) => {
469                return KernelResult::Error {
470                    code: KernelErrorCode::Validation,
471                    message: format!("chunk {sequence} is not valid base64: {e}"),
472                    detail: None,
473                };
474            }
475        };
476        // The frame limit already bounds this to something a few times larger.
477        // Held to the contract's chunk size anyway, so the memory a single
478        // request can make the kernel hold is the number the contract states
479        // rather than whatever the framing happens to allow.
480        if chunk.len() > BLOB_CHUNK_BYTES {
481            return KernelResult::Error {
482                code: KernelErrorCode::Validation,
483                message: format!(
484                    "chunk {sequence} carries {} bytes, over the {BLOB_CHUNK_BYTES}-byte chunk",
485                    chunk.len()
486                ),
487                detail: None,
488            };
489        }
490        match self.blobs().write_chunk(upload_id, sequence, &chunk).await {
491            Ok(()) => KernelResult::BlobChunkAccepted {
492                upload_id: upload_id.clone(),
493                sequence,
494            },
495            Err(e) => blob_refusal(&e),
496        }
497    }
498
499    /// One page of a projection, as the checkpoint would have canonicalized it.
500    ///
501    /// `cursor` and `exact` are the two ways to narrow the same query: a page
502    /// continues after a key, a get names one. Passing both is meaningless and
503    /// no caller does — the SQL would simply AND them.
504    async fn projection_page(
505        &self,
506        kind: ProjectionKind,
507        cursor: Option<&str>,
508        exact: Option<&str>,
509        rows: u32,
510    ) -> Result<(Vec<ProjectionRecord>, bool, &'static str)> {
511        let (query, key) = crate::checkpoint::read_query(kind).ok_or_else(|| {
512            KernelError::Config(format!("projection {} has no table", kind.as_str()))
513        })?;
514        let mut conn = self.connection().await?;
515        let raw: Vec<String> = sqlx::query_scalar(query)
516            .bind(cursor)
517            .bind(exact)
518            .bind(i64::from(rows))
519            .fetch_all(&mut *conn)
520            .await
521            .map_err(|e| KernelError::Config(format!("read {} projections: {e}", kind.as_str())))?;
522
523        let mut records = Vec::with_capacity(raw.len());
524        for line in raw {
525            // The same round trip the checkpoint makes, and for the same
526            // reason: `deny_unknown_fields` turns a column with no contract
527            // field into a refusal here, rather than a value that quietly never
528            // reaches the client.
529            records.push(
530                serde_json::from_str::<ProjectionRecord>(&line).map_err(|e| {
531                    KernelError::Config(format!(
532                        "a {} row does not match the contract type: {e}",
533                        kind.as_str()
534                    ))
535                })?,
536            );
537        }
538        let (records, cut) = fit_page(records)?;
539        Ok((records, cut, key))
540    }
541
542    /// The fresh-epoch proof: one genesis event, at whatever sequence the
543    /// DATABASE gave it, and nothing else in the log.
544    async fn verify_sealed(&self, sealed: bool) -> Result<KernelResult> {
545        let mut conn = self.connection().await?;
546        let row = sqlx::query(
547            "SELECT event_id, seq::text AS seq_text FROM gwk.event \
548             WHERE aggregate_type = $1 AND aggregate_id = $2 AND event_type = $3 \
549             ORDER BY seq LIMIT 1",
550        )
551        .bind(KERNEL_AGGREGATE)
552        .bind(KERNEL_SINGLETON)
553        .bind(GENESIS_EVENT_TYPE)
554        .fetch_optional(&mut *conn)
555        .await
556        .map_err(|e| KernelError::Config(format!("read genesis: {e}")))?
557        .ok_or_else(|| KernelError::Config("the log has no genesis event".to_owned()))?;
558
559        let event_id: String = row
560            .try_get("event_id")
561            .map_err(|e| KernelError::Config(format!("genesis event_id: {e}")))?;
562        let seq_text: String = row
563            .try_get("seq_text")
564            .map_err(|e| KernelError::Config(format!("genesis seq: {e}")))?;
565        // Never assumed to be 1: the sequence is database-assigned, and a
566        // restored or re-created log can legitimately start higher.
567        let genesis_watermark = crate::numeric::from_numeric_text(&seq_text)
568            .map(Seq::new)
569            .map_err(|e| KernelError::Config(format!("genesis seq: {e}")))?;
570
571        let count: i64 = sqlx::query_scalar("SELECT count(*) FROM gwk.event")
572            .fetch_one(&mut *conn)
573            .await
574            .map_err(|e| KernelError::Config(format!("count events: {e}")))?;
575
576        Ok(KernelResult::SealedVerification {
577            sealed,
578            genesis_event_id: EventId::new(event_id),
579            genesis_watermark,
580            // A COUNT, unrelated to the sequence above — one is "how many", the
581            // other is "which position", and a fresh epoch has count 1 at
582            // whatever position the database chose.
583            event_count: EventCount::new(count.max(0) as u64),
584        })
585    }
586}
587
588/// The live subscriptions of one connection, and the queues they write through.
589///
590/// Per-connection rather than per-daemon because every bound here is about one
591/// client's behaviour: how many streams it may hold, and how much the kernel
592/// will buffer for it before deciding it has stopped reading.
593struct Subscriptions<'a> {
594    daemon: &'a Daemon,
595    /// Dropping this aborts every subscription on it, which is what makes a
596    /// closed connection stop reading the database — a stream parked between
597    /// polls has nothing to notice a hangup by.
598    live: tokio::task::JoinSet<()>,
599    batches: mpsc::Sender<Outgoing>,
600    responses: mpsc::Sender<ServerControl>,
601    /// An admitted subscription waiting to be started. See [`Self::admit`].
602    admitted: Option<(RequestId, Option<Seq>)>,
603}
604
605impl<'a> Subscriptions<'a> {
606    fn new(
607        daemon: &'a Daemon,
608        responses: mpsc::Sender<ServerControl>,
609        batches: mpsc::Sender<Outgoing>,
610    ) -> Self {
611        Self {
612            daemon,
613            live: tokio::task::JoinSet::new(),
614            batches,
615            responses,
616            admitted: None,
617        }
618    }
619
620    /// Take a subscription request, without starting it.
621    ///
622    /// Starting is [`Self::start`]'s, one step later, because a subscription's
623    /// first batch must not overtake the response that announced it. The task
624    /// begins only once `Subscribed` is on the response queue, and the writer
625    /// drains that queue first, so the client sees the acknowledgement before
626    /// anything acknowledged by it.
627    fn admit(&mut self, request_id: &RequestId, cursor: Option<Seq>) -> KernelResult {
628        // Ended subscriptions are still in the set until something reaps them,
629        // and counting a slow consumer's corpse against a client's allowance
630        // would make the cap tighten over the life of a connection.
631        while self.live.try_join_next().is_some() {}
632        if self.live.len() >= MAX_SUBSCRIPTIONS_PER_CONNECTION {
633            // The connection and every stream already on it are untouched: this
634            // refuses one request, it does not punish the session.
635            return KernelResult::Error {
636                code: KernelErrorCode::Overloaded,
637                message: format!(
638                    "this connection already holds {MAX_SUBSCRIPTIONS_PER_CONNECTION} subscriptions"
639                ),
640                detail: None,
641            };
642        }
643        self.admitted = Some((request_id.clone(), cursor));
644        // Echoes the cursor asked for, which is where the stream starts. A
645        // client that sent none gets none back and learns where it is from the
646        // first batch.
647        KernelResult::Subscribed { cursor }
648    }
649
650    /// Start whatever [`Self::admit`] took, now that its response is queued.
651    fn start(&mut self) {
652        let Some((request_id, cursor)) = self.admitted.take() else {
653            return;
654        };
655        // Seeded with where the stream starts, so a subscription that ends
656        // before a single frame left reports its own starting point rather than
657        // sending the client back to the beginning of the log.
658        let delivered = Arc::new(std::sync::atomic::AtomicU64::new(
659            cursor.map_or(0, |seq| seq.value()),
660        ));
661        self.live.spawn(subscribe::run(
662            Arc::clone(&self.daemon.store),
663            request_id,
664            cursor,
665            self.batches.clone(),
666            self.responses.clone(),
667            self.daemon.wake.subscribe(),
668            delivered,
669        ));
670    }
671}
672
673/// A blob failure as a refusal the client can branch on.
674fn blob_refusal(error: &BlobError) -> KernelResult {
675    let code = match error {
676        BlobError::NotFound => KernelErrorCode::NotFound,
677        // Permanent, and answered even while ciphertext cleanup is still
678        // pending: a shredded blob never becomes readable again.
679        BlobError::Tombstoned => KernelErrorCode::BlobTombstoned,
680        // One code for both, because they are the same claim from two sides:
681        // the bytes are not the bytes they were said to be.
682        BlobError::DigestMismatch { .. } | BlobError::Integrity(_) => {
683            KernelErrorCode::BlobIntegrity
684        }
685        // No request on this wire removes a blob, so a pin cannot block one
686        // here. Mapped rather than dismissed, so the match stays exhaustive over
687        // the port's errors and a later request that CAN reach it gets a code.
688        BlobError::Pinned => KernelErrorCode::Validation,
689        BlobError::Storage(_) => KernelErrorCode::Storage,
690    };
691    KernelResult::Error {
692        code,
693        // The port's own wording, which already names the address, the sequence,
694        // or the two digests. Rephrasing it here would only lose that.
695        message: error.to_string(),
696        detail: None,
697    }
698}
699
700/// Drive one connection: handshake, then requests until the peer hangs up.
701///
702/// After the handshake the two directions are driven by two loops, and the first
703/// of them to finish ends the connection: the reader finishing means the peer
704/// hung up, the writer finishing means the transport failed, and neither is
705/// worth continuing half of. Cancelling the reader mid-frame is safe for exactly
706/// that reason — nothing is going to read the rest of it.
707pub async fn serve_connection<R, W>(
708    daemon: &Daemon,
709    reader: &mut R,
710    writer: &mut W,
711) -> std::result::Result<(), WireError>
712where
713    R: AsyncRead + Unpin,
714    W: AsyncWrite + Unpin,
715{
716    let mut ingress = Budget::new(
717        CONNECTION_INGRESS_BYTES_PER_WINDOW,
718        CONNECTION_EGRESS_BYTES_PER_WINDOW,
719    );
720    let readiness = daemon
721        .readiness()
722        .await
723        .map_err(|e| WireError::new(KernelErrorCode::Storage, format!("readiness: {e}")))?;
724    hello::negotiate(reader, writer, &mut ingress, readiness).await?;
725
726    // One allowance per direction, now that a direction is a loop. They were
727    // never coupled — a `spend` only ever touches the counter for the direction
728    // it charges — so the split changes nothing but which loop owns which half.
729    let mut egress = Budget::new(
730        CONNECTION_INGRESS_BYTES_PER_WINDOW,
731        CONNECTION_EGRESS_BYTES_PER_WINDOW,
732    );
733    let (responses_tx, responses_rx) = mpsc::channel(RESPONSE_QUEUE_DEPTH);
734    let (batches_tx, batches_rx) = mpsc::channel(BATCH_QUEUE_DEPTH);
735    let mut subs = Subscriptions::new(daemon, responses_tx, batches_tx);
736
737    tokio::select! {
738        read = read_requests(daemon, reader, &mut ingress, &mut subs) => read,
739        written = write_frames(writer, responses_rx, batches_rx, &mut egress) => written,
740    }
741}
742
743/// Read requests and queue their answers, until the peer hangs up.
744async fn read_requests<R>(
745    daemon: &Daemon,
746    reader: &mut R,
747    budget: &mut Budget,
748    subs: &mut Subscriptions<'_>,
749) -> std::result::Result<(), WireError>
750where
751    R: AsyncRead + Unpin,
752{
753    loop {
754        let frame = match read_frame(reader, FRAME_BODY_MAX_BYTES, budget).await? {
755            Incoming::Frame(frame) => frame,
756            Incoming::Closed => return Ok(()),
757        };
758        if frame.kind != FrameKind::Json {
759            return Err(WireError::new(
760                KernelErrorCode::Handshake,
761                format!("kind {:?} carries no control value", frame.kind),
762            ));
763        }
764        let control: gwk_domain::protocol::ClientControl = strict::decode(&frame.body)?;
765        let (request_id, request) = match control {
766            gwk_domain::protocol::ClientControl::Request {
767                request_id,
768                request,
769            } => (request_id, request),
770            // A second hello is a client that lost track of its own session.
771            // Refused rather than re-negotiated: re-running the handshake
772            // mid-connection would let the settled minor and capability set
773            // change under requests already in flight.
774            gwk_domain::protocol::ClientControl::Hello { .. } => {
775                return Err(WireError::new(
776                    KernelErrorCode::Handshake,
777                    "a second hello on an established connection",
778                ));
779            }
780        };
781
782        let result = daemon.answer(&request_id, &request, subs).await;
783        let response = ServerControl::Response { request_id, result };
784        if subs.responses.send(response).await.is_err() {
785            // The writer is gone, so nothing can be answered any more. Its own
786            // error is the one worth reporting and it already returned it.
787            return Ok(());
788        }
789        subs.start();
790    }
791}
792
793/// Own the write half, and take work from the two queues that feed it.
794///
795/// Responses first, always. A `StreamClosed` travels on the response queue for
796/// this reason: the thing it has to get past is precisely a batch queue that a
797/// stopped consumer has left full, and a fair writer would leave the client
798/// waiting on the frame that explains why nothing else is coming.
799async fn write_frames<W>(
800    writer: &mut W,
801    mut responses: mpsc::Receiver<ServerControl>,
802    mut batches: mpsc::Receiver<Outgoing>,
803    budget: &mut Budget,
804) -> std::result::Result<(), WireError>
805where
806    W: AsyncWrite + Unpin,
807{
808    loop {
809        let outgoing = tokio::select! {
810            biased;
811            Some(control) = responses.recv() => Outgoing { control, delivered: None },
812            Some(outgoing) = batches.recv() => outgoing,
813            // Both queues closed: the reader is done and no subscription is
814            // left to produce anything.
815            else => return Ok(()),
816        };
817        let body = serde_json::to_vec(&outgoing.control).map_err(|e| {
818            WireError::new(KernelErrorCode::Storage, format!("serialize a frame: {e}"))
819        })?;
820        write_frame(writer, FrameKind::Json, &body, budget).await?;
821        // AFTER the write, and only on success — the whole value of this number
822        // is that nothing is claimed delivered before it left.
823        if let Some((cell, seq)) = outgoing.delivered {
824            cell.store(seq, std::sync::atomic::Ordering::Release);
825        }
826    }
827}
828
829/// What a clean stop managed to do, for the caller to report.
830///
831/// The checkpoint is reported rather than returned as an error because a
832/// snapshot that fails must not keep the socket on the filesystem or the writer
833/// lock held — the next kernel would inherit both. It is reported rather than
834/// swallowed because a barrier that has silently stopped firing grows recovery
835/// time without bound, and the first anyone hears of it is a restart that
836/// replays the whole log.
837#[derive(Debug, Default)]
838pub struct Stopped {
839    /// The sequence the parting snapshot was taken at, if one was.
840    pub checkpoint: Option<Seq>,
841    /// Why there is no checkpoint, when the reason is a failure rather than
842    /// having nothing to snapshot.
843    pub checkpoint_error: Option<String>,
844}
845
846/// Accept until `shutdown` resolves, then stop accepting, drain, and checkpoint.
847///
848/// The order is the contract's: acceptance stops FIRST, so no connection starts
849/// during the drain, then in-flight work gets at most [`DRAIN_TIMEOUT_SECS`],
850/// then the projections are snapshotted at the watermark — after the drain, so
851/// the snapshot includes the last append rather than racing it — and only then
852/// is the socket removed, and only if it is still the file this process created.
853pub async fn run<S>(listener: Listener, daemon: Arc<Daemon>, shutdown: S) -> Result<Stopped>
854where
855    S: std::future::Future<Output = ()> + Send,
856{
857    // Before the first connection, so a subscription opened on it is on the
858    // notified path rather than the poll path from its first read.
859    daemon.notify_on_append();
860
861    let mut connections = tokio::task::JoinSet::new();
862    let shutdown = std::pin::pin!(shutdown);
863    let mut shutdown = shutdown;
864
865    loop {
866        tokio::select! {
867            // Biased so a pending shutdown wins a ready connection: on the way
868            // down, accepting one more is the wrong answer even when it is
869            // available.
870            biased;
871            () = &mut shutdown => break,
872            accepted = listener.accept() => {
873                let (stream, _peer) = accepted?;
874                let daemon = Arc::clone(&daemon);
875                connections.spawn(async move {
876                    let (mut reader, mut writer) = tokio::io::split(stream);
877                    // A connection's failure is its own: a malformed frame from
878                    // one client must not take down the daemon serving the
879                    // others.
880                    let _ = serve_connection(&daemon, &mut reader, &mut writer).await;
881                });
882            }
883        }
884    }
885
886    let drained = tokio::time::timeout(std::time::Duration::from_secs(DRAIN_TIMEOUT_SECS), async {
887        while connections.join_next().await.is_some() {}
888    })
889    .await;
890    if drained.is_err() {
891        // Abandoned rather than waited on forever: the socket still has to come
892        // off and the writer lock still has to be released, and a client that
893        // stopped reading cannot be allowed to hold either.
894        connections.shutdown().await;
895    }
896
897    // After the drain: an in-flight append that commits during it belongs in the
898    // snapshot, and the barrier this takes would otherwise be waiting on it
899    // anyway.
900    let mut stopped = Stopped::default();
901    match daemon.store.checkpoint_at_watermark().await {
902        Ok(at) => stopped.checkpoint = at,
903        Err(e) => stopped.checkpoint_error = Some(e.to_string()),
904    }
905
906    listener.remove();
907    Ok(stopped)
908}
909
910/// One `UnixStream`, served. The split is here rather than in
911/// [`serve_connection`] so the codec stays testable over any pair of pipes.
912pub async fn serve_stream(
913    daemon: &Daemon,
914    stream: UnixStream,
915) -> std::result::Result<(), WireError> {
916    let (mut reader, mut writer) = tokio::io::split(stream);
917    serve_connection(daemon, &mut reader, &mut writer).await
918}
919
920#[cfg(test)]
921mod tests {
922    use super::*;
923
924    #[test]
925    fn a_page_is_cut_by_bytes_and_never_to_nothing() {
926        // Enough half-kilobyte items to pass the budget several times over.
927        let items: Vec<String> = (0..8_000).map(|i| format!("{i:0>512}")).collect();
928        let (kept, cut) = fit_page(items).expect("measure");
929        assert!(cut, "a page far past the budget was not cut");
930        let bytes: usize = kept
931            .iter()
932            .map(|s| serde_json::to_vec(s).expect("serialize").len())
933            .sum();
934        // The item that pushed the running total over is not kept, so what
935        // survives is inside the budget rather than one item past it.
936        assert!(bytes <= PAGE_BYTE_BUDGET, "the kept page is over budget");
937        assert!(bytes > PAGE_BYTE_BUDGET / 2, "the cut threw away too much");
938
939        // The first item survives even alone over the budget: a page that
940        // returned nothing would leave the cursor standing still and the client
941        // asking the same question forever.
942        let (kept, cut) = fit_page(vec!["x".repeat(PAGE_BYTE_BUDGET * 2)]).expect("measure");
943        assert_eq!(kept.len(), 1);
944        assert!(
945            !cut,
946            "one item is not a cut page — there is nothing behind it"
947        );
948    }
949}