Expand description
Event → projection reducer (design.md §1.4).
Each event mutates zero or more projection files. Unknown kinds are
ignored for forward compatibility. The reducer expects to run under the
per-run flock.
Idempotency contract (per design.md §7.3 at-least-once delivery):
*.created reducers short-circuit when their projection file already
exists; status/resolution reducers are no-ops once the terminal state
has been reached. Replaying the same event stream against existing
projections is therefore a clean no-op-or-apply.
This idempotence is load-bearing for the applied_seq watermark
(append-then-apply atomicity; see crate::schema::Manifest::applied_seq
and crate::events::append_and_apply_event). Of the two options the spec
offered — make the reducer idempotent, OR have the writer skip events
already reflected in the projection — we chose idempotent reducer: the
existence/terminal guards already present here mean the catch-up replay can
re-fold any tail event (one whose projection landed before a crash, or one
whose projection did not) with the same no-op-or-apply outcome, so the
writer needs no per-event “already applied?” probe. The watermark advances
only after an event’s projections are fsynced, so it can lag the projections
but never lead them.
§Manifest counters are derived, not folded
The reducers here deliberately do not touch the manifest’s denormalized
counters (node_count, open_discussions, pending_spinoffs). Those are
recomputed from the projection directories by
derive_counters, invoked from
advance_applied_seq at the
watermark advance. An earlier design incremented/decremented them inside
these reducers, but a crash between a projection write and the follow-on
manifest.json write could permanently desync them: the replay re-folded
the event, hit the *.created/terminal idempotency guard above, and skipped
the counter mutation that never actually landed. Deriving the counts makes
drift impossible — there is no delta to lose. See issue
manifest-counter-desync. A count-affecting reducer still emits its manifest
op to refresh updated_at; the counter fields it carries are overwritten by
the derive step.
Because a count-affecting event rewrites a projection file and the
manifest counter under the same exclusive flock, a reader that scans both
together must hold the shared flock (LOCK_SH) for the whole scan or it
could see the projection change without the matching counter (or vice
versa). See crate::projections and design.md §4.
Constants§
- VIA_
EXPLICIT_ MERGE - The
viamarkerrun mergestamps on the terminalnode.reportit appends after a clean merge. This is the octl-cli/octl-core contract point: the CLI (crates/octl-cli/src/run/merge.rs) writes it and the reducer reads it here to decide adoption. Kept in core so the reducer’s adoption gate and the supervisor’s teardown gate (supervise/cleanup.rs) agree on the exact string.
Functions§
- plan_
projections - Enumerate the projection files the reducer would write for
event, in the ordercommit_opswould write them, without performing any write.