Pre-existing uncommitted work: ha-repl-hardening payload data plane (7.x), heartbeat liveness fix (Req 1.3), connection-reuse ASAN fixes, doc cleanup. New: ha-consistency-invariants umbrella (requirements/design/research) and its four implementation sub-specs (specs generated via batch, cross-spec review applied), steering roadmap + product.md Raft+CP decision.
41 KiB
Design: ha-commit-barrier-rollout
Sub-spec 2 of 4 of the ha-consistency-invariants umbrella. This design slices the umbrella design's Write-Path Classification, Config Barrier, and Idempotency Guards components (
.kiro/specs/ha-consistency-invariants/design.md), implementing umbrella Migration Strategy phase 2. The umbrella's WAL-type classification table and Reply-Ordering Contract are the contract; this design records their call-site verification (which the umbrella explicitly mandated) and the enforcement mechanism. The commit engine interface (include/commit.h,commit_submitWAIT/RETAIN) is consumed exactly as shipped by ha-commit-engine — this spec never modifiescommit.cinternals. Requirement IDs below are THIS spec's IDs (seerequirements.mdfor the umbrella mapping).
Overview
Purpose: This feature extends the majority-commit barrier from job submission to every cluster-state-changing operation: request-shaped operations reply to their requester only after commit (and fail on timeout), fact-shaped operations are appended and retained until committed with no timeout, configuration upgrades fail honestly to the admin tool, and job state transitions become idempotent so retained-fact retry and replay are safe.
Users: Cluster operators gain the guarantee that any confirmed control operation survives failover and that worker-reported reality is never silently dropped; downstream sub-specs (ha-single-leader-role, ha-convergence-recovery) gain a write path that is barrier-complete before the transitional leader state is removed.
Impact: Adds a classification dispatch at the persistence hook seam (persist.c) with a fail-fast guard for unclassified WAL types; restructures do_jobcontrol_op (job.c:921-934), do_queuecontrol_op (queue.c:119-132), do_hostcontrol_op (worker.c:689-702), and handle_worker_join (join.c:33-119) to commit-first-then-reply; admits fact-bearing opcodes through the dispatch gate during quorum loss (ha.c:1527); propagates the config majority-ACK result from push_config_to_standbys (config_repl.c:866-930) to the upgradeConfig requester (config_repl.c:1057-1094); adds state-machine guards to mark_job_* (job.c:583/638/696/726); and retires the legacy walAck counting fallback in repl_wait_majority_ack (repl.c:386-462) while hardening its configAck branch.
Goals
- Commit-first-then-reply for every request-shaped control handler;
handle_jobsubmit(job.c:495-581) is the reference implementation and stays unchanged - Fact WAL types routed RETAIN (log-is-the-queue): appended + broadcast, pending purely by
lsn > committed_lsn, never dropped while running+leading, accepted even during quorum loss - An unclassified state-changing WAL type cannot silently bypass the barrier (debug assert; production fact-fallback)
- Config upgrade fails to the requester on missed majority and does not restart into the failed config
mark_job_*idempotent and ordered (duplicate = no-op; terminal→non-terminal rejected)
Non-Goals
- Commit engine internals: per-LSN evaluation, admission cap,
committed_lsnadvancement (ha-commit-engine — consumed as-is) - Promotion no-op / own-term gate consumers / CTRL_PRIMARY_COMMITTING removal (ha-single-leader-role; the gate's COMMITTING branch keeps rejecting facts until that spec deletes the state — known residual)
- Demotion truncate+restart, rejoin reconciliation, egress watermark filter, ACK-driven catch-up, quorum-transition logging (ha-convergence-recovery)
- Worker protocol changes; recovery of facts that never reached the controller or died with it (worker-rejoin-resync backlog — umbrella-locked: crash-loss of uncommitted facts accepted until then)
- WAL_NOOP introduction (ha-single-leader-role adds the type; the classification guard here forces it to be classified when it lands)
Boundary Commitments
This Spec Owns
- The WAL-type → commit-mode classification table, its dispatch at the persistence hook seam, and the unclassified-type guard
- The reply ordering of every request-shaped control handler on the primary (job control, queue control, worker control, worker join/update) and the receipt-ACK verdict for scheduler-origin handlers
- The commit-barrier helper (
persist_commit_barrier) that handlers use to realize WAIT-mode completion - Fact admission through the dispatch gate's no-write-quorum path (opcode allowlist only — not quorum accounting)
- Config-change barrier error propagation:
push_config_to_standbysreturn contract, configAck majority arithmetic, and theupgradeConfigcaller's failure path mark_job_*transition guards- Retirement of the legacy (engine-less) walAck counting in
repl_wait_majority_ack
Out of Boundary
commit.c/include/commit.h— consumed, never modified (interface changes there trigger revalidation here)ha_has_write_quorumand quorum accounting (ha-commit-engine); the gate's role-based branches other than the PRIMARY no-quorum opcode listcommitted_lsnadvancement rules, admission cap, membership re-evaluation (commit engine)- Egress filtering of provisional state to the scheduler (ha-convergence-recovery; dispatch of uncommitted submits is closed there)
- Standby-side apply, WAL wire format, catch-up streaming mechanics (ha-wal-replication)
- Worker/scheduler retry behavior on error replies (client-side, existing semantics)
Allowed Dependencies
commit_submit(ce, lsn, COMMIT_MODE_WAIT|RETAIN, timeout_ms)and the engine instance embedded incluster_t(ha-commit-engine, P0) — the only commit-evaluation authoritypersist_wal_async/persist_wal_sync_appendappend+broadcast primitives andpersist_t.current_lsn/committed_lsnatomics (persist.c, P0)repl_on_config_ack/acked_config_lsnper-standby config tracking (repl.h, P0) — read-only consumption for the config barrier- Hook dispatch (
xce_run_hooks, HOOK_AFTER_EVENT seam) — existing pattern, unchanged signatures - Threading rules per
ha-async-io.md: commit-barrier waits only on vtask threads; nothing here runs on iomgr - Dependency direction: handlers (job.c/queue.c/worker.c/join.c) → persist.c barrier/classification → commit engine; persist.c never calls back into handlers
Revalidation Triggers
- Any change to
commit.hsignatures or return codes (upstream) → re-verify barrier helper and handler error mapping - ha-single-leader-role adding WAL_NOOP or removing CTRL_PRIMARY_COMMITTING → classification table gains the WAL_NOOP row (RETAIN, umbrella-fixed); gate COMMITTING branch disappears
- Any new state-changing opcode/WAL type → must be added to the classification table (the debug assert makes this unmissable) and its handler swept for reply ordering
- worker-rejoin-resync landing → re-verify
mark_job_*guards against its replay ordering - Changes to
acked_config_lsnsemantics or the upgradeConfigAck payload → re-verify configAck majority arithmetic
Architecture
Existing Architecture Analysis
- All state-changing writes flow through one seam: entity mutation fires
HOOK_AFTER_EVENT, andpersist_job_hook_fun/persist_queue_hook_fun/persist_worker_hook_fun(persist.c:1093/1172/1204) map hook details to WAL types viadetail_to_wal_type(persist.c:1068-1088) and append. Only WAL_JOB_SUBMIT uses the sync barrier (persist.c:1107-1129); everything else ispersist_wal_asyncfire-and-forget - Request handlers reply immediately after mutation:
do_jobcontrol_op(job.c:932),do_queuecontrol_op(queue.c:130),do_hostcontrol_op(worker.c:700),handle_worker_join(join.c:109) — all violate umbrella 2.1 today.handle_jobsubmit(job.c:563-578) is the proven correct shape - Queue/worker/job-state hooks fire while the entity WRITE lock is held (queue.c:76-112, job.c:629-631/687-689); job-submit's hook runs after the job locks are released (job.c:553) — this asymmetry drives WAIT placement (below)
- Facts arrive via
do_notify(cluster.c:118-157, no reply) anddo_update_op/do_set_preason/do_clr_preason(receipt-style replies); the dispatch gate (ha.c:1527) rejectsOPCode_notifywith-XCE_ERR_NOT_READYunder quorum loss → permanent silent fact loss (bug-396 family, verified pre-existing) - Call-site verification of the umbrella classification table (mandated by the umbrella): WAL_JOB_STOP/RESUME are appended only from
on_job_stop/on_job_resume, which are invoked exclusively fromdo_notify(worker-reported stop/resume facts). Client job control (do_jobcontrol_op→job_ctrl, job.c:306-438) either forwards to the worker (running job — no local entry) or mutates locally viamark_job_update/mark_job_fail(not-started job → WAL_JOB_UPDATE/WAL_JOB_FAIL). WAL_JOB_UPDATE is absent from the umbrella table and is dual-origin (request: job_ctrl; fact: on_job_postdone, rusage update). Resolution: STOP/RESUME/UPDATE/FAIL classify as fact/RETAIN at the seam, and the umbrella's intent for client job control — "error to client" — is realized by the handler-level commit barrier indo_jobcontrol_op(the Reply-Ordering Contract's handler sequence is the umbrella-authoritative mechanism). Recorded in research.md push_config_to_standbys(config_repl.c:866-930) callsrepl_wait_majority_ack(configAck)at 916-917 but only logs the result and returns the push count; the caller (config_repl.c:1057) ACKs the admin tool with status=0 and restarts regardless. The configAck counting (repl.c:418-438) has the same defects the engine fixed for walAck:acked_config_lsn > 0(any previous ack counts),n_active == 0 → success, denominatorn_activeinstead ofn_peers
Architecture Pattern & Boundary Map
graph TB
subgraph requesters
CLI[client and cli ops]
SCHED[scheduler]
WK[worker]
ADM[admin tool]
end
subgraph controller_primary
GATE[ha dispatch gate<br/>fact allowlist]
H[request handlers<br/>job queue worker join]
F[fact handlers<br/>notify update preason]
HOOKS[persist hooks<br/>classification dispatch]
BAR[persist_commit_barrier]
CB[config barrier<br/>config_repl]
CE[commit engine<br/>consumed]
GUARD[mark_job guards]
end
CLI --> GATE
SCHED --> GATE
WK --> GATE
ADM --> GATE
GATE --> H
GATE --> F
H --> GUARD
F --> GUARD
GUARD --> HOOKS
HOOKS -->|append plus RETAIN register| CE
H -->|before success reply| BAR
BAR -->|commit_submit WAIT| CE
ADM -.upgradeConfig.-> CB
CB -->|configAck majority| CE
Architecture Integration (sliced from umbrella):
- Selected pattern: classification at the single persistence seam + completion at the handler. The seam guarantees no entry type escapes classification (2.1, 2.2); the handler realizes the umbrella Reply-Ordering Contract sequence "validate → mutate (provisional) → WAL append → commit_submit(WAIT) → reply" (1.1)
- WAIT placement decision: WAL_JOB_SUBMIT keeps its in-hook sync barrier (hook runs lock-free, engine-wired upstream). All other request-shaped types complete via
persist_commit_barrier()called by the handler after entity locks are released — blocking a commit wait under a queue/job/worker WRITE lock would stall readers for up to the commit timeout. Same completion semantics, umbrella-conformant placement (the contract names the handler sequence) - Existing patterns preserved: hook-based persistence dispatch, provisional in-memory mutation order (umbrella-locked),
errnoxearly-return error style, full fan-out broadcast - Steering compliance: barrier waits only on vtask threads (handlers run on the vtask pool); iomgr untouched; TDD with testsuite/ tests first
Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| Backend / Services | C (gnu17), pthreads, stdatomic | classification, barrier helper, handler restructure | no new dependencies, no .capnp changes |
| Data / Storage | existing WAL (persist.c), wal_types.h enum | classification keyed on wal_entry_type + worker detail |
no on-disk format change |
| Build / Test | meson, testsuite/ pattern | new test_commit_barrier_rollout.c |
XCE_TESTING seams |
File Structure Plan
Directory Structure
src/controller/
├── persist.c # MODIFIED — classification dispatch, RETAIN registration,
│ # persist_commit_barrier, unclassified guard
├── job.c # MODIFIED — do_jobcontrol_op reply ordering; mark_job_* guards
├── queue.c # MODIFIED — do_queuecontrol_op reply ordering
├── worker.c # MODIFIED — do_hostcontrol_op reply ordering
├── join.c # MODIFIED — handle_worker_join reply ordering
├── ha.c # MODIFIED — dispatch-gate fact allowlist (PRIMARY branch only)
├── config_repl.c # MODIFIED — push_config_to_standbys return contract; caller failure path
├── repl.c # MODIFIED — legacy walAck counting retired; configAck branch hardened
└── testsuite/
└── test_commit_barrier_rollout.c # NEW — classification, barrier ordering, fact retention,
# config barrier, idempotency guards
src/include/persist.h # MODIFIED — wal_commit_mode_t enum, wal_classify(),
# persist_commit_barrier() declarations
src/include/repl.h # MODIFIED — configAck evaluation reuses the entry_lsn
# parameter (added by ha-commit-engine) as the
# config push tag; no new parameter
Modified Files (what and why)
src/include/persist.h— declareswal_commit_mode_t { WAL_MODE_WAIT, WAL_MODE_RETAIN, WAL_MODE_LOCAL },wal_commit_mode_t wal_classify(uint32_t wal_type, int detail),int persist_commit_barrier(persist_t *p, uint32_t timeout_ms)src/controller/persist.c—wal_classifytable; all three hook functions consult it: WAIT → existing sync path (submit) or async append with handler-owed barrier; RETAIN →persist_wal_async+commit_submit(RETAIN)registration; LOCAL → unchanged (WAL_CHECKPOINT); unclassified →assert(0)under XCE_TESTING, ERROR + RETAIN fallback in production.persist_commit_barrierimplemented here (snapshotcurrent_lsn,commit_submit(WAIT))src/controller/job.c—do_jobcontrol_op(job.c:921-934): barrier betweenjob_ctrlsuccess and the reply.mark_job_start/finish/fail/update(job.c:583/638/696/726): transition guards before mutationsrc/controller/queue.c—do_queuecontrol_op(queue.c:119-132): barrier betweenqueue_ctrlsuccess and the replysrc/controller/worker.c—do_hostcontrol_op(worker.c:689-702): barrier betweenworker_ctrlsuccess and the replysrc/controller/join.c—handle_worker_join(join.c:33-119): barrier between the AFTER_EVENT hook and the reply at join.c:107-109; join failure (idx < 0) keeps replying immediatelysrc/controller/ha.c—ha_dispatch_gatePRIMARY no-quorum allowlist (ha.c:1537-1556): addOPCode_notify,OPCode_update,OPCode_setpreason,OPCode_clrpreasonsrc/controller/config_repl.c—push_config_to_standbysreturns the barrier result (0 /-ETIMEDOUT), push count via out-param; passes the in-flight tag for per-push evaluation; callerdo_upgrade_config_with_bytes(config_repl.c:1057): on failure send error reply, skip restartsrc/controller/repl.c— walAck branch ofrepl_wait_majority_ack: legacy counting removed (engine is mandatory on this path;ce == NULLbecomes a programming error); configAck branch: per-push comparison (acked_config_lsn >= push_tag), majority overn_peerscounting self,n_peers > 1with zero standby acks never succeedssrc/controller/meson.build— registertestsuite/test_commit_barrier_rollout.c
System Flows
Request-shaped control operation (job/queue/worker control, worker join)
sequenceDiagram
participant R as requester
participant V as vtask thread handler
participant E as entity lock scope
participant P as persist hooks
participant CE as commit engine
R->>V: control request
V->>V: dispatch gate incl write quorum
V->>E: lock, validate, mutate provisional
E->>P: HOOK_AFTER_EVENT append entries
P->>CE: persist_wal_async + register
E->>V: unlock, rc
alt rc != 0
V->>R: error reply
else rc == 0
V->>CE: persist_commit_barrier WAIT
alt committed
V->>R: success reply
else timeout or not ready
V->>R: error reply
end
end
Flow decisions: the barrier waits on a snapshot of current_lsn taken after the mutation returns — single-writer appends and contiguous commit advancement guarantee the snapshot covers every entry the operation produced (1.1, 1.4); waiting may also cover unrelated concurrent entries (bounded by the commit timeout, harmless — they must commit anyway); -ETIMEDOUT/-XCE_ERR_NOT_READY map to the error reply via the existing errnox style (1.2, 1.3); no in-memory rollback (provisional model, umbrella-locked — outcome converges per umbrella 2.5, owned downstream).
Fact-shaped delivery (worker notify, rusage update, preason, worker dead)
Gate admits the opcode even without write quorum (3.2) → handler applies the transition through the mark_job_* guards (5.1-5.3) → hook classifies the WAL type RETAIN → persist_wal_async append + broadcast + commit_submit(RETAIN) (returns immediately, no timeout) → handler returns (notify: no reply; update/preason: receipt-style reply unchanged, verdict per 2.3/2.4). The fact is pending purely by lsn > committed_lsn; catch-up streaming re-delivers the uncommitted suffix and committed_lsn advances when ACKs resume (3.1, 3.3, 3.4). No new queue structure exists (log-is-the-queue, umbrella-locked).
Configuration upgrade
sequenceDiagram
participant A as admin tool
participant U as do_upgrade_config_with_bytes
participant PS as push_config_to_standbys
participant S as standbys
A->>U: OP_upgradeConfig
U->>U: validate, save to disk, bump generation
U->>PS: push bytes, in-flight tag
PS->>S: parallel push threads
S-->>PS: OP_upgradeConfigAck tag
PS->>PS: majority of n_peers acked tag, self counted
alt majority within timeout
PS-->>U: 0, acked count
U->>U: committed_config_tag advanced
U->>A: success reply
U->>U: request restart
else timeout
PS-->>U: -ETIMEDOUT
U->>A: error reply upgrade failed
U->>U: no restart, tag not committed
end
Flow decisions: committed_config_tag advances only inside the majority branch (4.3 — already correct, preserved); the failure path uses the existing send_upgrade_error shape so the admin tool sees a definite failure (4.2); restart is gated on success (4.4 — closes the roadmap residual "don't restart before the commit barrier passes"); config remains a parallel, simpler track sharing only the majority arithmetic rule with the engine (umbrella-locked: one in-flight config change at a time).
Requirements Traceability
| Requirement | Summary | Components | Flows |
|---|---|---|---|
| 1.1 | success reply only after majority commit | Reply-Ordering Sweep, Commit Barrier Helper | request control |
| 1.2 | commit timeout → error reply | Commit Barrier Helper, Reply-Ordering Sweep | request control |
| 1.3 | not-ready rejection → error reply | Commit Barrier Helper (engine code passthrough) | request control |
| 1.4 | no success-interpretable reply pre-commit | Reply-Ordering Sweep (integration-test assertion) | request control |
| 2.1 | every WAL type classified WAIT/RETAIN | Write-Path Classification | both write flows |
| 2.2 | unclassified type fails fast / fact-fallback | Write-Path Classification (guard) | — |
| 2.3 | dispatch ACK is receipt-only | Reply-Ordering Sweep (handle_jobdispatch verdict) | — |
| 2.4 | exhaustive reply-site verification | Reply-Ordering Sweep (audit) | — |
| 3.1 | facts appended, retried, no timeout | Write-Path Classification (RETAIN routing) | fact delivery |
| 3.2 | facts accepted during quorum loss | Fact Gate Admission | fact delivery |
| 3.3 | uncommitted facts never discarded | Write-Path Classification (RETAIN; log-is-the-queue) | fact delivery |
| 3.4 | facts commit after quorum restore, no redelivery | Write-Path Classification + existing catch-up (verified by test) | fact delivery |
| 4.1 | config confirmed only after majority | Config Barrier | config upgrade |
| 4.2 | config timeout → failed to requester | Config Barrier | config upgrade |
| 4.3 | unacked config never committed | Config Barrier (tag advance in success branch only) | config upgrade |
| 4.4 | failed upgrade does not restart | Config Barrier (caller) | config upgrade |
| 5.1 | duplicate transition is logged no-op | Idempotency Guards | fact delivery |
| 5.2 | terminal→non-terminal rejected with WARN | Idempotency Guards | fact delivery |
| 5.3 | replay-once equivalence | Idempotency Guards | fact delivery |
Components and Interfaces
| Component | Layer | Intent | Req Coverage | Key Dependencies | Contracts |
|---|---|---|---|---|---|
| Write-Path Classification | controller/persist.c | route every WAL type to its commit mode at the hook seam | 2.1, 2.2, 3.1, 3.3, 3.4 | commit engine (P0), hooks (P0) | Service |
| Commit Barrier Helper | controller/persist.c | handler-callable WAIT completion on the appended watermark | 1.1, 1.2, 1.3 | commit engine (P0) | Service |
| Reply-Ordering Sweep | controller/job.c, queue.c, worker.c, join.c | commit-first-then-reply in every request handler; receipt-ACK verdicts | 1.1, 1.2, 1.3, 1.4, 2.3, 2.4 | Commit Barrier Helper (P0) | Service |
| Fact Gate Admission | controller/ha.c | admit fact-bearing opcodes without write quorum | 3.2 | dispatch gate (P0) | Service |
| Config Barrier | controller/config_repl.c, repl.c | per-push config majority with error propagation | 4.1, 4.2, 4.3, 4.4 | repl_on_config_ack tracking (P0) | Service |
| Idempotency Guards | controller/job.c | safe duplicate/replay of job transitions | 5.1, 5.2, 5.3 | — | Service |
Write-Path Classification (persist.c, src/include/persist.h)
| Field | Detail |
|---|---|
| Intent | Single seam where every state-changing WAL append is classified and registered with the engine |
| Requirements | 2.1, 2.2, 3.1, 3.3, 3.4 |
Classification table (umbrella table verified against call sites — verification deviations recorded in research.md §2; this verified table is the implementation contract):
| WAL type (+ detail) | Verified origin | Shape | Mode | Failure behavior |
|---|---|---|---|---|
| WAL_JOB_SUBMIT | handle_jobsubmit (client) | request | WAIT (existing in-hook sync path) | error to client (reference impl, unchanged) |
| WAL_QUEUE_CTRL | do_queuecontrol_op (client) | request | WAIT (handler barrier) | error to client |
| WAL_WORKER_CTRL, detail JOIN/UPDATE | handle_worker_join, do_hostcontrol_op | request | WAIT (handler barrier) | error reply to worker/client |
| WAL_WORKER_CTRL, detail DEAD | controller-detected (worker.c:565/578) | fact | RETAIN | none — retained until committed |
| WAL_JOB_START / FINISH / FAIL / STOP / RESUME / UPDATE / RUSAGE / CKPT / PREASON | worker notify, scheduler preason, postdone, rusage; FAIL/UPDATE also from job_ctrl (request origin → covered by do_jobcontrol_op's handler barrier) | fact | RETAIN | none — retained until committed |
| WAL_NOOP (future) | promotion (ha-single-leader-role) | internal | RETAIN | added downstream; guard forces classification |
| WAL_CHECKPOINT | local checkpoint marker | local-only | LOCAL | unchanged (never replicated, by design) |
Responsibilities & Constraints
wal_classify(wal_type, detail)is a pure lookup;detaildisambiguates only WAL_WORKER_CTRL (JOIN/UPDATE vs DEAD)- Hook dispatch: RETAIN →
persist_wal_async(append + broadcast, the surviving RETAIN fast path) followed bycommit_submit(ce, appended_lsn, COMMIT_MODE_RETAIN, 0)registration (returns immediately; observability counter). WAIT → WAL_JOB_SUBMIT keepspersist_wal_sync_append; other WAIT types append viapersist_wal_asyncand completion is owed by the handler barrier (placement decision, Architecture) - Unclassified guard: under
XCE_TESTING,assert(0)with the offending type logged; in production,ERRORlog + RETAIN treatment (never dropped, never falsely confirmed) (2.2) - Fact appends never return commit errors to their callers —
persist_job_hook_fun's non-submit return value stays the local-append rc only (3.1)
Dependencies
- Inbound: hook system (HOOK_AFTER_EVENT) — all state-changing writes (P0)
- Outbound:
commit_submit(P0);persist_wal_async/persist_wal_sync_append(P0)
Contracts: Service [x]
Service Interface
/* src/include/persist.h */
typedef enum {
WAL_MODE_WAIT, /* request-shaped: barrier before requester reply */
WAL_MODE_RETAIN, /* fact-shaped: retained until committed, no timeout */
WAL_MODE_LOCAL, /* not replicated (WAL_CHECKPOINT) */
} wal_commit_mode_t;
/* Pure classification lookup. detail disambiguates WAL_WORKER_CTRL only.
* Unknown type: XCE_TESTING assert; production returns WAL_MODE_RETAIN
* after ERROR log. */
wal_commit_mode_t wal_classify(uint32_t wal_type, int detail);
- Preconditions: called at the hook seam with the type produced by
detail_to_wal_type - Postconditions: total over all
wal_entry_typevalues; deterministic - Invariants: no WAL append path bypasses classification
Commit Barrier Helper (persist.c, declared in src/include/persist.h)
| Field | Detail |
|---|---|
| Intent | Give request handlers a single call realizing commit_submit(WAIT) on everything their mutation appended |
| Requirements | 1.1, 1.2, 1.3 |
Responsibilities & Constraints
- Snapshot
lsn = p->current_lsn(underp->lock, matchingpersist_wal_sync_append's read at persist.c:1019-1021); ifcommitted_lsn >= lsnalready, return 0 without blocking - Delegate to
commit_submit(ce, lsn, COMMIT_MODE_WAIT, timeout_ms ? timeout_ms : p->config.sync_timeout_ms); return its code unmodified (0 /-ETIMEDOUT/-XCE_ERR_NOT_READY) - Single-writer + contiguous advancement ⇒ the snapshot covers every entry the calling operation produced; over-waiting on unrelated concurrent entries is accepted (bounded by the timeout; they must commit anyway).
n_peers <= 1returns 0 immediately (engine self-quorum, standalone behavior preserved) - Must be called only from vtask threads after entity locks are released; counts against the engine's admission cap (deliberate CP back-pressure)
- Migration completion: with all request paths on the engine (directly or via this helper), the walAck legacy counting in
repl_wait_majority_ackis removed (upstream marked it "retired by ha-commit-barrier-rollout"); the configAck branch remains the config track's mechanism
Contracts: Service [x]
Service Interface
/* src/include/persist.h */
/* Block until committed_lsn covers everything appended so far.
* timeout_ms = 0 → p->config.sync_timeout_ms.
* Returns 0 (committed), -ETIMEDOUT (not committed in time — entry
* remains in log; outcome converges per umbrella 2.5, owned downstream),
* -XCE_ERR_NOT_READY (admission cap / own-term gate, retriable). */
int persist_commit_barrier(persist_t *p, uint32_t timeout_ms);
- Preconditions: commit engine wired (
cluster_initphase, upstream); vtask thread; no entity locks held - Postconditions: returns 0 ⇔
committed_lsn >=the snapshot watermark - Invariants: never sends replies, never mutates state
Reply-Ordering Sweep (job.c, queue.c, worker.c, join.c)
| Field | Detail |
|---|---|
| Intent | Enforce the umbrella Reply-Ordering Contract at every control-path reply site |
| Requirements | 1.1, 1.2, 1.3, 1.4, 2.3, 2.4 |
Responsibilities & Constraints
- Restructure pattern (uniform):
rc = <ctrl fn>(...); if (rc == 0) rc = persist_commit_barrier(&c->persist, 0); reply.status = rc; xce_reply(...). Applies to:do_jobcontrol_op(job.c:921-934) — covers the not-started branch's WAL_JOB_UPDATE/WAL_JOB_FAIL entries; the running-job branch (forward to worker, no local entry) waits on an already-covered watermark and returns immediatelydo_queuecontrol_op(queue.c:119-132)do_hostcontrol_op(worker.c:689-702)handle_worker_join(join.c:33-119) — barrier inserted between the AFTER_EVENT hook (join.c:104-105) and the success reply (join.c:107-109); error replies (validate_host, hostconfig, idx < 0) stay immediate;on_worker_joindataex push stays after the reply
- Receipt-ACK verdicts (verified, kept, classification-guard-enforced):
handle_jobdispatch(job.c:459-491) — early reply at job.c:466-471 is ACK-of-receipt;dispatch_a_job_produces no WAL entries (allocation results return vianotify_dispatch, and resulting job-state changes arrive later as worker facts) → per the umbrella contract the early reply stands (2.3).do_set_preason/do_clr_preason(job.c:1040/1068) — scheduler-origin facts (WAL_JOB_PREASON, RETAIN); early reply is receipt-only.do_update_oppaths (opupd.c) — worker-origin facts (rusage → WAL_JOB_RUSAGE/WAL_JOB_UPDATE RETAIN; resvals produce no WAL); replies are receipt-only - Exhaustive audit (2.4): every
xce_replycall site under src/controller/ (excluding testsuite) is enumerated by grep and assigned one of {post-commit confirmation, receipt-only ACK, read-only/replication/error reply}; the audit table is committed with the task evidence. Known control-path population at design time: job.c:471,524,569,578,932,1050,1078; queue.c:130; worker.c:700; join.c:28,54,66,109,139,152,198,225,248,257,283; opupd.c:48,64,88; cluster.c (gate/shutdown/hello/notify-path replies); config_repl.c:619-1129 (config/replication track); ha.c:1035; opget.c:222 (read-only); reply.c:61; task.c:97 — line numbers re-verified by grep during implementation - A missed handler silently violates umbrella 2.1 — the integration test asserting no-success-reply-before-commit is the behavioral backstop (1.4)
Contracts: Service [x] — external reply semantics: success now implies majority durability; new error codes -ETIMEDOUT/-XCE_ERR_NOT_READY surface to requesters through existing reply envelopes (Go tier passes them through, per umbrella out-of-boundary)
Implementation Notes
- Integration: handlers run on vtask threads (cluster.c dispatch switch at cluster.c:325-363) — barrier blocking is steering-compliant
- Validation: integration test drives each restructured opcode against a 3-node-simulated registry with ACKs withheld → error reply,
committed_lsnunmoved; with ACKs → success reply strictly after commit - Risks: scheduler/worker callers seeing new timeout errors where they previously saw instant success — deliberate CP behavior (umbrella); retry semantics unchanged on their side
Fact Gate Admission (ha.c)
Summary-only: add OPCode_notify, OPCode_update, OPCode_setpreason, OPCode_clrpreason to the CTRL_PRIMARY no-write-quorum allowlist in ha_dispatch_gate (ha.c:1537-1556) so reported facts reach the RETAIN path during quorum loss (3.2 — closes the bug-396 silent-loss hole; umbrella error strategy: "fact-shaped ops are still appended and retained"). Request-shaped opcodes (jobsub, jobcontrol, queuecontrol, hostcontrol, jobdispatch, join) remain rejected without quorum. The CTRL_PRIMARY_COMMITTING branch is intentionally untouched (deleted wholesale by ha-single-leader-role — known residual until then). Quorum accounting itself is out of boundary (upstream).
Config Barrier (config_repl.c, repl.c configAck branch)
| Field | Detail |
|---|---|
| Intent | Make the config majority-ACK result authoritative for the upgrade outcome |
| Requirements | 4.1, 4.2, 4.3, 4.4 |
Responsibilities & Constraints
push_config_to_standbys(config_repl.c:866-930) signature:int push_config_to_standbys(cluster_t *c, const uint8_t *bytes, size_t blen, const uint8_t sha256[32], uint64_t new_tag, int *acked_out)— returns the barrier result (0 /-ETIMEDOUT), push count viaacked_out. The majority wait (config_repl.c:916-917) passesnew_tagdown for per-push evaluation;committed_config_tagadvance stays inside the success branch only (config_repl.c:918-922 — 4.3 preserved)- configAck branch of
repl_wait_majority_ack(repl.c:418-438) hardened to mirror the engine's rules on the config track (umbrella: "same majority arithmetic, parallel simpler track"): count standbys withacked_config_lsn >= push_tag(not> 0— a previous upgrade's ack no longer counts), majority =1 (self) + count > n_peers / 2over configured members (notn_active),n_peers <= 1→ immediate success,n_peers > 1with zero covering acks →-ETIMEDOUTat deadline (then_active == 0 → successshort-circuit is removed from this branch). The configAck branch reuses theentry_lsnparameter already added torepl_wait_majority_ackby ha-commit-engine as the config push tag — no second parameter is added (repl.hcomment updated accordingly); the function is now configAck-only (walAck callers are on the engine) - Caller
do_upgrade_config_with_bytes(config_repl.c:1056-1094): on non-zero return —send_upgrade_error(ep, req, c, ...)to the admin tool (4.2), skip the success ACK, skiprestart_requested(4.4). Disk save and generation bump have already happened (pre-existing sequence); the reply makes the failure explicit and the cluster-wide tag stays uncommitted — convergence/rollback of a half-pushed config remains the existing OP_replConfig anti-entropy behavior (out of boundary) - One in-flight config change at a time (umbrella-locked) — no per-entry registry needed
Contracts: Service [x] — upgradeConfig reply contract: success ⇔ majority-acked config; failure carries an error string and implies no restart
Implementation Notes
- Integration:
CONF_PUSH_TIMEOUT_MSremains the config commit timeout - Validation: integration test withholds configAcks → admin reply is an error,
committed_config_tagunmoved, no restart request; with acks → success and tag advanced - Risks: admin tooling that treated any reply as success must observe
status— existing CLI already surfaces reply status (admin-config-tool backlog item consumes this contract)
Idempotency Guards (job.c)
| Field | Detail |
|---|---|
| Intent | Make job state transitions safe under duplicate, out-of-order, and replayed application |
| Requirements | 5.1, 5.2, 5.3 |
Responsibilities & Constraints
- Guard placement: at the top of
mark_job_start(job.c:583),mark_job_finish(job.c:638),mark_job_fail(job.c:696),mark_job_update(job.c:726), under the caller-held job WRITE lock, before the BEFORE_EVENT hook fires (a guarded-out transition runs no hooks → no WAL entry, no double-fire) - Rules (terminal = any of DONE | EXIT, with or without PERR/PDONE modifiers):
- duplicate: requested resulting state == current state → INFO log, return 0, no mutation, no hooks (5.1)
- backward: current state terminal AND requested state non-terminal (START on finished job, UPDATE clearing terminal bits) → WARN log, return
-EALREADY-style rejection, state preserved (5.2) - forward/normal: proceed unchanged;
mark_job_fail's existing post-exec special case (job.c:702-716) already tolerates fail-after-finish and is kept as the terminal-state-compatible path
- Replay equivalence (5.3): guards make
restore_job_state-driven re-application and RETAIN retry converge to single-application state; callers (on_job_*,job_ctrl) treat the duplicate code as success-no-op - Shared prerequisite of worker-rejoin-resync (umbrella adjacent expectation) — guards live here, resync consumes them later
Contracts: Service [x] — mark_job_* return contract gains the logged no-op/rejection outcomes; all existing callers checked during the sweep
Data Models
Domain Model (changes only)
- wal_commit_mode_t — new enum (persist.h); pure classification metadata, never persisted
- WAL entries, job_t, queue_t, worker_t — no format or field changes in this spec (
submit_lsntagging is ha-convergence-recovery's egress filter, not here) - Invariants: every appended state-changing entry is classified; a success reply to a request-shaped operation implies
committed_lsn >=all entries that operation appended;committed_config_tagadvances only on per-push config majority; job state never transitions terminal → non-terminal
Error Handling
Error Strategy
- Request-shaped commit failure:
-ETIMEDOUT/-XCE_ERR_NOT_READYpropagate to the requester reply via the existingerrnoxearly-return pattern; no in-memory rollback (provisional model, umbrella-locked; convergence owned downstream) - Fact-shaped paths never surface commit errors to reporters: local append failure (disk) keeps its existing ERROR logging; commit progress is the engine's concern (RETAIN)
- Unclassified WAL type: XCE_TESTING assert (fail fast in CI); production ERROR + RETAIN fallback (2.2 — fail-safe toward retention, never silent confirmation)
- Config failure: explicit error reply via
send_upgrade_error; no restart; WARN with tag and acked count - Guard rejections: INFO (duplicate) / WARN (backward) with jobid and both states — log-once per application, no log storms
Monitoring
- Retained-uncommitted depth (committed-vs-appended lag) — engine counter, consumed in logs here for fact-backlog visibility
- Per-handler barrier failures WARN with opcode and rc; config barrier WARN already present (config_repl.c:924-926), now coupled to the failed reply
Testing Strategy
Unit Tests (testsuite/test_commit_barrier_rollout.c, XCE_TESTING)
- Classification totality: every
wal_entry_typevalue maps to a mode; WAL_WORKER_CTRL detail split (JOIN/UPDATE → WAIT, DEAD → RETAIN); unknown type asserts under XCE_TESTING (2.1, 2.2) - Barrier helper: returns 0 immediately when
committed_lsncovers the watermark;-ETIMEDOUTwith ACKs withheld andcommitted_lsnunmoved; 0 after covering ACK;n_peers <= 1immediate success (1.1, 1.2, 1.3) - Idempotency guards: duplicate FINISH → state unchanged, rc = no-op, no AFTER hook fired (no WAL entry); START on DONE job → rejected, WARN, state preserved; double application of a transition equals single application; fail-after-finish keeps existing PERR behavior (5.1, 5.2, 5.3)
- configAck arithmetic: stale
acked_config_lsnfrom a previous tag does not count; zero covering acks with n_peers=3 → timeout; majority with self-count → 0; n_peers=1 → immediate (4.1, 4.3)
Integration Tests (testsuite/)
- No-success-reply-before-commit: for each restructured opcode (jobcontrol, queuecontrol, hostcontrol, worker join), with ACKs withheld the reply is an error and arrives only at/after the timeout; with ACKs the success reply is observable only after
committed_lsncovers the operation's entries (1.1, 1.2, 1.4) - Fact retention across quorum loss: simulate quorum loss → notify-driven transitions still append (RETAIN, no error), gate admits the fact opcodes; restore ACKs → entries commit without re-delivery (3.1-3.4)
- Dispatch receipt-ACK: handle_jobdispatch still replies immediately; no WAL entry is produced by dispatch processing itself (classification assert clean) (2.3)
- Config barrier: upgradeConfig with withheld configAcks → error reply to requester,
committed_config_tagunmoved, no restart requested; with acks → success, tag advanced (4.1-4.4) - Reply-site audit (2.4): scripted grep over all
xce_replycall sites in control paths, each assigned a category; zero unclassified sites
Regression
- Full controller testsuite + ASAN (
/xce-build controller) green; existing job-submit barrier tests (upstream) unaffected;test_repl_majority.cconfigAck cases updated to the hardened per-push arithmetic
Performance & Scalability
- Request-control latency gains one majority RTT (the same cost job submit already pays); fact paths gain no latency (RETAIN returns immediately)
- Concurrent blocked handlers are bounded by the engine's
n_vcpus-relative admission cap (upstream); excess load degrades to fast NOT_READY rejections — deliberate CP - Barrier over-wait on concurrent unrelated entries is bounded by
sync_timeout_msand only occurs while uncommitted traffic is pending
Migration Strategy
This spec is phase 2 of the umbrella's hard-ordered landing ("barrier before state removal"):
- Classification + RETAIN registration + barrier helper (no behavior change for replies yet)
- Handler restructure sweep + fact gate admission (reply semantics switch on)
- Config barrier error propagation; legacy walAck counting retirement; idempotency guards Each step leaves the system no less consistent than before it; rollback trigger per step: controller testsuite regression or job-control E2E failure. ha-single-leader-role must not land until the sweep (step 2) is complete and audited.