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}