--- id: "bug-1564" status: "fixed" status_updated: "2026-09-24" status_note: "" component: "controller" severity: "unknown" timestamp: "2026-08-17T07:15:00.000Z" last_seen: "2026-09-19T09:00:00Z" occurrences: 1 files: ["src/controller/persist.c (persist_flush)"] tags: ["persist", "durability", "commit-engine", "callback", "wait-timeout", "regression", "bug-1563-followup"] related: ["bug-1563"] legacy: true --- # bug-1564: controller_unit_test: test_durable_majority_commit_end_to_end FAIL: Expected 0 Was -110… ## Description controller_unit_test: test_durable_majority_commit_end_to_end FAIL: Expected 0 Was -110 (commit_submit(WAIT) must return 0 after durable-majority) -- deterministic 3/3 direct runs of the binary, not a flake. Surfaced while live-testing an unrelated /xce-deploy-ha enhancement, right after rebuilding controller on top of bug-1563's fix. ## Environment _Not recorded (migrated from buglog.json)._ ## Reproduction _Not recorded (migrated from buglog.json)._ ## Root cause persist_flush() (the synchronous durability point tests and clean-shutdown use in place of waiting for the background flusher) advances durable_lsn directly under p->lock but never calls p->on_durable_advance -- only fsync_worker's own successful flusher_flush_once() call fires that callback (persist.c, 'after > before' gate). Any WAIT-blocked commit caller (commit_engine's COMMIT_MODE_WAIT) that relies on on_durable_advance to be re-evaluated is therefore never woken by a persist_flush()-driven advance -- only by fsync_worker's own next successful cycle. This gap was LATENT and never triggered before bug-1563: pre-1563, fsync_worker's un-throttled retry loop (the livelock bug-1563 fixed) spun continuously while work was pending, so it happened to notice any state change (e.g. a test clearing test_force_fsync_fail) and independently re-attempt+succeed+fire the callback within microseconds, completely masking that persist_flush's own advance was silently un-observed. bug-1563's fix (correct: fsync_worker now backs off up to wal_sync_ms between failed attempts instead of spinning) removed that accidental fast-repoll cover, so a persist_flush()-only advance during a slow-cadence window (make_durability_persist uses wal_sync_ms=5000 deliberately, 'durability comes from the explicit flush, not the timer') now genuinely times out the 2000ms-budget WAIT with nothing left to wake it for the remaining ~4.7s fsync_worker was still asleep. ## Fix FIXED (TDD RED->GREEN). Added test_persist_flush_fires_durable_advance_callback (test_commit_durability.c, next to test_durable_majority_commit_end_to_end): registers a counting callback via persist_set_durable_cb, appends an entry, asserts persist_flush() fires it exactly once on a genuine advance and not again on a no-op flush. RED confirmed against the pre-fix persist_flush (0 fires). Fix: persist_flush() now fires p->on_durable_advance(p->durable_cb_ctx) after releasing p->lock, gated on advanced==true (only when durable_lsn actually moved) -- identical gate and identical lock-ordering discipline (strictly outside p->lock, so the callback's own ce->lock acquisition has no p->lock held underneath it) to fsync_worker's own existing callback-firing code. GREEN: both test_persist_flush_fires_durable_advance_callback and the originally-broken test_durable_majority_commit_end_to_end pass, confirmed deterministic across 3 direct repeat runs of controller_unit_test. Full suite: 1104 tests, 2 failures (both pre-existing, unrelated overlayfs fdatasync sandbox flakiness in test_config_repl.c, unchanged from before this fix). ## Verification _Not recorded (migrated from buglog.json)._ ## References - Related: bug-1563