@@ -9,6 +9,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
1010### Added
1111
12+ - ** Per-subscriber fail-closed delivery semantics** (` epoch_core ` , ` epoch_pg ` , CLOUD-216) —
13+ an opt-in ` FailureMode { FailOpen (default), FailClosed } ` letting a subscriber halt
14+ rather than silently skip an event it cannot apply in order:
15+ - ** ` epoch_core ` ** — ` FailureMode ` enum (` #[non_exhaustive] ` , re-exported from the
16+ prelude); defaulted ` failure_mode() ` on ` EventObserver ` , ` Projection ` , and ` Saga ` ,
17+ forwarded by ` ProjectionHandler ` , ` SagaHandler ` , ` SagaAdapter ` , and the
18+ ` impl Saga for Arc<S> ` blanket. Existing implementors are unchanged (default ` FailOpen ` ).
19+ - ** Live batch path** — a ` FailClosed ` subscriber that hits a deserialize failure or an
20+ observer-retry exhaustion holds its contiguous checkpoint below the bad sequence
21+ (writing a ` unrecoverable: deserialize: … ` DLQ row for the deserialize case), fires
22+ the new ` on_halt ` callback once on entry, and self-heals on the next batch once the
23+ cause is fixed. A panicking observer is now contained (caught in
24+ ` process_event_with_retry ` ) instead of killing the listener task — a deliberate
25+ fail-open behaviour change: the panic routes through the existing retry/DLQ
26+ machinery.
27+ - ** Catch-up and drain paths** — the same hold semantics apply during initial catch-up
28+ and inline-drain: a ` FailClosed ` subscriber that hits a bad row while catching up
29+ halts at that row (the subscriber is still registered and the live listener takes
30+ over from there), and self-heals once the cause is fixed.
31+ - ** Gap refusal** — a ` FailClosed ` subscriber refuses the ` gap_timeout ` backstop: if
32+ the backstop would advance past an unproven gap, the subscriber halts with
33+ ` HaltReason::GapUnproven ` and stays held until the gap is proven permanent (fence
34+ clears) or an operator releases it. The fence-clear branch is untouched: a gap
35+ proven never to have existed advances under both modes.
36+ - ** Wedge isolation** — a halted ` FailClosed ` subscriber is excluded from the shared
37+ event-window floor and served by its own private re-seeding fetch each cycle, so a
38+ permanently wedged subscriber cannot pin the delivery window of healthy peers.
39+ - ** ` PgEventBus::release_halt(subscriber_id, past_sequence) ` ** — new operator API
40+ for advancing a wedged subscriber's persisted checkpoint past a held sequence it
41+ never finished. Forward-only (rejects ` past_sequence ` at or below the current
42+ checkpoint with ` PgEventBusError::BackwardRelease ` ). Fires the ` on_halt ` callback
43+ with ` HaltReason::Released ` on success. Events already applied above the released
44+ position are folded in without re-delivery.
45+ - ** ` ReplayAlways ` contiguous HWM** (CLOUD-227) — a ` FailClosed ` ` ReplayAlways `
46+ subscriber now tracks a contiguous high-water mark that advances only across the
47+ unbroken prefix of successfully applied sequences, matching the checkpoint semantics
48+ of ` Checkpointed ` subscribers. A wedged ` ReplayAlways ` subscriber is likewise
49+ excluded from the shared floor; its remedy is a fresh ` subscribe() ` call for a full
50+ replay (no ` release_halt ` — ` ReplayAlways ` has no persisted checkpoint row).
51+ - New public API in ` epoch_pg::event_bus ` : ` HaltCallback ` , ` HaltInfo ` , ` HaltReason `
52+ (` #[non_exhaustive] ` , variants: ` DeserializeFailure ` , ` ObserverFailure ` ,
53+ ` GapUnproven ` , ` Released ` ). No schema migration.
54+
1255- ** Subscriber readiness + startup safety** (` epoch_core ` , ` epoch_pg ` , CLOUD-221) —
1356 first-class lag/readiness API on ` PgEventBus ` , a ` ReplayAlways ` subscription mode for
1457 in-memory projections that replay from zero every boot, and startup catch-up so a
@@ -212,9 +255,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
212255 ` setup_trigger() ` once per bus to drop the legacy fixed-name trigger; leaving it in
213256 place is not unsafe, only redundant — it produces one duplicate NOTIFY per insert,
214257 which is discarded by the per-event checkpoint check.
215- - ** BREAKING** (` epoch_pg ` ): ` PgEventBusError ` gains two new variants,
216- ` SubscriberNotFound ` and ` InlineDispatchNotSupported ` . The enum is not
217- ` #[non_exhaustive] ` , so a downstream exhaustive ` match ` on it will stop compiling.
258+ - ** BREAKING** (` epoch_pg ` ): ` PgEventBusError ` gains three new variants,
259+ ` SubscriberNotFound ` , ` InlineDispatchNotSupported ` , and ` BackwardRelease `
260+ (returned by ` release_halt ` when ` past_sequence ` is at or below the current
261+ persisted checkpoint). The enum is not ` #[non_exhaustive] ` , so a downstream
262+ exhaustive ` match ` on it will stop compiling.
218263- Released the ` projections ` lock across the listener's batch drain instead of
219264 holding it for the whole backlog (` epoch_pg ` , CLOUD-225): every readiness method
220265 also locks ` projections ` , so ` subscriber_lag ` , ` wait_until_caught_up ` , and
@@ -248,6 +293,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
248293 ` on_gap_timeout: Option<Arc<dyn GapTimeoutCallback>> ` field (defaults to ` None ` ).
249294 Code that constructs ` ReliableDeliveryConfig ` using struct-literal syntax (rather than
250295 ` ..Default::default() ` ) must add ` on_gap_timeout: None ` to the literal.
296+ - ** Source-compat note** (` epoch_pg ` , CLOUD-216, ` feat(pg)! ` ): ` ReliableDeliveryConfig `
297+ gains the new ` on_halt: Option<Arc<dyn HaltCallback>> ` field (defaults to ` None ` ),
298+ fired when a fail-closed subscriber halts delivery. Code that constructs
299+ ` ReliableDeliveryConfig ` using struct-literal syntax (rather than
300+ ` ..Default::default() ` ) must add ` on_halt: None ` to the literal.
251301- ** Source-compat note** : ` ReliableDeliveryConfig ` (` epoch_pg ` ) gains the new
252302 ` snapshot_fencing: bool ` field (defaults to ` true ` ). Code that constructs
253303 ` ReliableDeliveryConfig ` using struct-literal syntax (rather than
0 commit comments