You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Expert review round 2 (2026-07-27) findings 2, 3 and part of the vacuum API feedback. Deliberately deferred to this issue by maintainer decision; the interim mitigations shipped in the fix series are documented below.
Problems
Shutdown: PersistenceTask owns a detached engine task. Dropping the task previously either leaked the task (pre-0014: a reopened table raced the old engine on the same files) or aborted it unconditionally (0014: AbortHandle::abort() can cancel mid-batch — the batch sub-futures are not cancellation-safe; a data page can be left half-written while its index events are abandoned). Interim shipped: Drop aborts only when the engine is provably idle (check_wait_triggers), otherwise logs tracing::error and leaves the task running; callers must wait_for_ops() before dropping.
Permanent event gaps: BatchOperation::validate strictly defers on event-id gaps (correct — events are positional; force-applying scrambles on-disk nodes). A gap that never fills (only possible if events are discarded after id assignment, i.e. a non-CDC mutation on a persisted index — see Non-CDC index mutations silently burn CDC event ids #167) stalls persistence forever: wait_for_ops() hangs, tracing::error fires repeatedly after 8 attempts. A log line is not an API-visible failure, and the error repeats each attempt.
Partial batch application: apply_batch_operation drains all three sub-operations and reports every failed component (data write / primary index / secondary index, each error wrapped with its component name). But the engine loop only tracing::warns and continues to the next batch from potentially inconsistent durable state. Blind retry is not safe either — structural CDC events are not idempotent.
VacuumPersistence::apply_move is infallible — it cannot report a closed/failed engine once such states exist.
Transition atomically to Failed on unrecoverable gap (attempts threshold) or batch-apply error; stop applying later operations; reject new pushes (including apply_move); wake all waiters; preserve root cause.
wait_for_ops() returns Result (breaking change to the generated API and every test — mechanical but wide) or panics on Failed if a non-breaking variant is preferred.
pub async fn close(self) -> Result<(), PersistenceError>: stop intake, drain per documented semantics, signal the engine loop to exit, await the JoinHandle, surface terminal errors. Drop stays as the guarded last resort.
This also fixes the pre-existing hang class where the engine task panics (see the TOC bug history: engine death made wait_for_ops hang until tests added timeouts).
Verification hints
tests/persistence/vacuum.rs and tests/persistence/bulk_load_stall.rs show the timeout-instead-of-hang pattern. Reverting the one-line .with_persistence(..) wiring in the persist impls generator produces a genuine permanent gap on demand (stall repro). The suite is fully deterministic (40/40 clean runs at time of filing), so any flakiness introduced by lifecycle work will be visible immediately.
Context
Expert review round 2 (2026-07-27) findings 2, 3 and part of the vacuum API feedback. Deliberately deferred to this issue by maintainer decision; the interim mitigations shipped in the fix series are documented below.
Problems
PersistenceTaskowns a detached engine task. Dropping the task previously either leaked the task (pre-0014: a reopened table raced the old engine on the same files) or aborted it unconditionally (0014:AbortHandle::abort()can cancel mid-batch — the batch sub-futures are not cancellation-safe; a data page can be left half-written while its index events are abandoned). Interim shipped:Dropaborts only when the engine is provably idle (check_wait_triggers), otherwise logstracing::errorand leaves the task running; callers mustwait_for_ops()before dropping.BatchOperation::validatestrictly defers on event-id gaps (correct — events are positional; force-applying scrambles on-disk nodes). A gap that never fills (only possible if events are discarded after id assignment, i.e. a non-CDC mutation on a persisted index — see Non-CDC index mutations silently burn CDC event ids #167) stalls persistence forever:wait_for_ops()hangs,tracing::errorfires repeatedly after 8 attempts. A log line is not an API-visible failure, and the error repeats each attempt.apply_batch_operationdrains all three sub-operations and reports every failed component (data write / primary index / secondary index, each error wrapped with its component name). But the engine loop onlytracing::warns and continues to the next batch from potentially inconsistent durable state. Blind retry is not safe either — structural CDC events are not idempotent.VacuumPersistence::apply_moveis infallible — it cannot report a closed/failed engine once such states exist.Required design (from review)
Failedon unrecoverable gap (attempts threshold) or batch-apply error; stop applying later operations; reject new pushes (includingapply_move); wake all waiters; preserve root cause.wait_for_ops()returnsResult(breaking change to the generated API and every test — mechanical but wide) or panics onFailedif a non-breaking variant is preferred.pub async fn close(self) -> Result<(), PersistenceError>: stop intake, drain per documented semantics, signal the engine loop to exit, await the JoinHandle, surface terminal errors.Dropstays as the guarded last resort.wait_for_opshang until tests added timeouts).Verification hints
tests/persistence/vacuum.rsandtests/persistence/bulk_load_stall.rsshow the timeout-instead-of-hang pattern. Reverting the one-line.with_persistence(..)wiring in the persist impls generator produces a genuine permanent gap on demand (stall repro). The suite is fully deterministic (40/40 clean runs at time of filing), so any flakiness introduced by lifecycle work will be visible immediately.