Skip to main content

Module reducer

Module reducer 

Source
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 via marker run merge stamps on the terminal node.report it 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 order commit_ops would write them, without performing any write.