Expand description
Events-daemon split (ADR-170): the audit lane leaves the domain store.
The ADR-133 idempotent audit batch is the measured bulk of event write
volume, and in a single-store deployment its rows queue on the same SQLite
writer lane as domain mutations. This module moves that lane into a
dedicated events daemon that owns the events database, reachable over a
Unix socket with the same length-prefixed framing and peer-uid admission the
main daemon socket uses. Plain event appends stay on the domain store —
the legacy events table has raw-SQL consumers (schedule provenance, kg
projection guards, graph-query substrate unions) whose correctness
depends on finding those rows there.
Cooperating pieces:
run_events_daemon— the server loop theevents-daemonsubcommand runs: binds the events socket, owns the only resident writer of the events database, and serves append/read requests through the ordinarySqlEventStore.EventsSplitClient— one per domain process. Plain appends ride a bounded in-memory queue drained by a background forwarder (fire-and-forget; overflow or a dead daemon drops the batch, counts it, and logs — the loss-tolerant durability class made concrete; unused by the default routing until telemetry producers opt in). Idempotent audit-batch appends and reads are synchronous framed round-trips with bounded timeouts, because their callers are background flushers or query paths, never the dispatch hot path.ForwardingEventStore— the lane-sideEventStoreover the socket. Preflight validation delegates to an in-memorySqlEventStore, so the ADR-133 audit-batch seam keeps its pre-enqueue shape check without any I/O on the dispatch path.SplitEventStore— the per-namespace handlecrate::runtime::KhiveRuntime::eventsreturns when the split is configured: routes the idempotent lane to the events store, plain appends to the legacy store, and merges reads across both.
Domain availability never depends on events-daemon liveness: every failure path here degrades (drop + count + log, or a typed storage error for the synchronous lanes) instead of blocking the caller.
Structs§
- Events
Daemon Guard - Advisory lock guaranteeing at most one events daemon per socket path. Held for the daemon’s lifetime; a second daemon exits instead of stealing the socket path from the live one.
- Events
Forwarding Metrics - Counters describing the fire-and-forget lane’s degradation. Zero drops is
the healthy state; any non-zero
dropped_batchesmeans the loss-tolerant contract was exercised and says so. - Events
Split Client - One per domain process: the connection to the events daemon plus the bounded fire-and-forget append queue.
- Events
Split Config - How a runtime reaches event storage when the split is configured.
- Forwarding
Event Store EventStoreimplementation the runtime hands out when the events split runs in daemon mode. Appends are fire-and-forget through the client’s bounded queue; the ADR-133 idempotent lane and all reads are synchronous round-trips to the events daemon.- Split
Event Store - The store the runtime hands out when the events split is configured: routes by APPEND CLASS rather than moving the whole event plane.
Enums§
- Events
Request - Request frame sent from a domain process to the events daemon.
- Events
Response - Response frame from the events daemon.
- Wire
Writer Task Failure - Whether a writer-state error belongs to one failed request on a still-live seam or to a permanently retired writer task.
- Wire
Writer Task State - Wire mirror of
khive_storage::WriterTaskRequestState. A separate type because the storage enum is not serializable and the wire shape must stay under this module’s protocol-version control, not the storage crate’s.
Constants§
- DEFAULT_
APPEND_ QUEUE_ BATCHES - Default bound on the fire-and-forget append queue, in batches. The byte bound below also applies, so a large batch cannot multiply this depth into unbounded retained event memory.
- DEFAULT_
APPEND_ QUEUE_ BYTES - Maximum serialized append-request bytes retained by the fire-and-forget queue and its one in-flight delivery. A single request must also fit the daemon’s per-frame cap; the queue budget covers several such requests.
- EVENTS_
PROTOCOL_ VERSION - Bump whenever the request or response frame shape changes incompatibly. The server rejects frames whose version it does not speak, so a skewed client gets a typed refusal instead of a deserialization panic.
- MAX_
QUERY_ EVENTS_ PAGE_ ROWS - Cap on
QueryEventspage size, in rows. The wirePageRequest.limitis client-suppliedu32, and the daemon materializes the full page as aVec<Event>and serializes it into one response frame — so an unbounded limit is attacker-controlled memory and serialization work in a process that lives for months. Over-cap requests get a typed refusal naming the cap, never a silently clamped page: a caller that asked for more rows than it got would otherwise read the short page as the end of the data. The split client’s merged read requests a prefix ofoffset + limitrows, so this cap also bounds the deep-offset window a socket client can demand in one request.
Functions§
- client_
for - The process-wide client for
socket_path, created (and its forwarder spawned) on first use. Requires a tokio runtime context on first call. - direct_
backend_ for - The process-wide direct (embedded-mode) backend for
db_path, opened read-write on first use. - direct_
backend_ read_ only_ for - The process-wide direct backend for
db_path, opened READ-ONLY on first use. The file must already exist — this constructor never creates or schema-initializes an events database, which is what a read-only runtime’s no-DB-creation contract requires of its event lane. - events_
db_ path_ beside - Default events database file, beside the main database file.
- events_
socket_ path_ beside - Events daemon socket path, beside the events database it serves.
- forwarding_
metrics - Forwarding metrics for the process-wide client at
socket_path, if one exists.Nonemeans the split never initialized in this process. - run_
events_ daemon - Serve the events daemon loop on
socket_path, owningdb_path. - supervise_
events_ daemon - Supervise the events daemon from the main daemon process: probe the socket periodically and (re)spawn the daemon subcommand when unreachable.
- try_
acquire_ events_ daemon_ guard - Try to become the events daemon for
socket_path.None= the lock could not be safely acquired — either another events daemon holds it, or a hardening step refused (symlinked lock entry, failed chmod). Both mean the caller must not serve; callers that need the socket directory validated must runensure_socket_dir_is_trustedon the parent BEFORE calling this, so no lock-path operation happens in an untrusted directory.