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.
446 lines
41 KiB
Markdown
446 lines
41 KiB
Markdown
# 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_submit` WAIT/RETAIN) is consumed exactly as shipped by ha-commit-engine — this spec never modifies `commit.c` internals. Requirement IDs below are THIS spec's IDs (see `requirements.md` for 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_lsn` advancement (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_standbys` return contract, configAck majority arithmetic, and the `upgradeConfig` caller'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_quorum` and quorum accounting (ha-commit-engine); the gate's role-based branches other than the PRIMARY no-quorum opcode list
|
|
- `committed_lsn` advancement 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 in `cluster_t` (ha-commit-engine, P0) — the only commit-evaluation authority
|
|
- `persist_wal_async` / `persist_wal_sync_append` append+broadcast primitives and `persist_t.current_lsn` / `committed_lsn` atomics (persist.c, P0)
|
|
- `repl_on_config_ack` / `acked_config_lsn` per-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.h` signatures 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_lsn` semantics 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`, and `persist_job_hook_fun` / `persist_queue_hook_fun` / `persist_worker_hook_fun` (persist.c:1093/1172/1204) map hook details to WAL types via `detail_to_wal_type` (persist.c:1068-1088) and append. Only WAL_JOB_SUBMIT uses the sync barrier (persist.c:1107-1129); everything else is `persist_wal_async` fire-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) and `do_update_op`/`do_set_preason`/`do_clr_preason` (receipt-style replies); the dispatch gate (ha.c:1527) rejects `OPCode_notify` with `-XCE_ERR_NOT_READY` under 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 from `do_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 via `mark_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 in `do_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) calls `repl_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`, denominator `n_active` instead of `n_peers`
|
|
|
|
### Architecture Pattern & Boundary Map
|
|
|
|
```mermaid
|
|
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), `errnox` early-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` — declares `wal_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_classify` table; 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_barrier` implemented here (snapshot `current_lsn`, `commit_submit(WAIT)`)
|
|
- `src/controller/job.c` — `do_jobcontrol_op` (job.c:921-934): barrier between `job_ctrl` success and the reply. `mark_job_start/finish/fail/update` (job.c:583/638/696/726): transition guards before mutation
|
|
- `src/controller/queue.c` — `do_queuecontrol_op` (queue.c:119-132): barrier between `queue_ctrl` success and the reply
|
|
- `src/controller/worker.c` — `do_hostcontrol_op` (worker.c:689-702): barrier between `worker_ctrl` success and the reply
|
|
- `src/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 immediately
|
|
- `src/controller/ha.c` — `ha_dispatch_gate` PRIMARY no-quorum allowlist (ha.c:1537-1556): add `OPCode_notify`, `OPCode_update`, `OPCode_setpreason`, `OPCode_clrpreason`
|
|
- `src/controller/config_repl.c` — `push_config_to_standbys` returns the barrier result (0 / `-ETIMEDOUT`), push count via out-param; passes the in-flight tag for per-push evaluation; caller `do_upgrade_config_with_bytes` (config_repl.c:1057): on failure send error reply, skip restart
|
|
- `src/controller/repl.c` — walAck branch of `repl_wait_majority_ack`: legacy counting removed (engine is mandatory on this path; `ce == NULL` becomes a programming error); configAck branch: per-push comparison (`acked_config_lsn >= push_tag`), majority over `n_peers` counting self, `n_peers > 1` with zero standby acks never succeeds
|
|
- `src/controller/meson.build` — register `testsuite/test_commit_barrier_rollout.c`
|
|
|
|
## System Flows
|
|
|
|
### Request-shaped control operation (job/queue/worker control, worker join)
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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; `detail` disambiguates only WAL_WORKER_CTRL (JOIN/UPDATE vs DEAD)
|
|
- Hook dispatch: RETAIN → `persist_wal_async` (append + broadcast, the surviving RETAIN fast path) followed by `commit_submit(ce, appended_lsn, COMMIT_MODE_RETAIN, 0)` registration (returns immediately; observability counter). WAIT → WAL_JOB_SUBMIT keeps `persist_wal_sync_append`; other WAIT types append via `persist_wal_async` and completion is owed by the handler barrier (placement decision, Architecture)
|
|
- Unclassified guard: under `XCE_TESTING`, `assert(0)` with the offending type logged; in production, `ERROR` log + 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
|
|
```c
|
|
/* 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_type` values; 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` (under `p->lock`, matching `persist_wal_sync_append`'s read at persist.c:1019-1021); if `committed_lsn >= lsn` already, 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 <= 1` returns 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_ack` is removed (upstream marked it "retired by ha-commit-barrier-rollout"); the configAck branch remains the config track's mechanism
|
|
|
|
**Contracts**: Service [x]
|
|
|
|
##### Service Interface
|
|
```c
|
|
/* 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_init` phase, 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 immediately
|
|
- `do_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_join` dataex 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 via `notify_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_op` paths (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_reply` call 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_lsn` unmoved; 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 via `acked_out`. The majority wait (config_repl.c:916-917) passes `new_tag` down for per-push evaluation; `committed_config_tag` advance 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 with `acked_config_lsn >= push_tag` (not `> 0` — a previous upgrade's ack no longer counts), majority = `1 (self) + count > n_peers / 2` over configured members (not `n_active`), `n_peers <= 1` → immediate success, `n_peers > 1` with zero covering acks → `-ETIMEDOUT` at deadline (the `n_active == 0 → success` short-circuit is removed from this branch). The configAck branch reuses the `entry_lsn` parameter already added to `repl_wait_majority_ack` by ha-commit-engine as the config push tag — no second parameter is added (`repl.h` comment 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, skip `restart_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_MS` remains the config commit timeout
|
|
- Validation: integration test withholds configAcks → admin reply is an error, `committed_config_tag` unmoved, 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_lsn` tagging 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_tag` advances 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_READY` propagate to the requester reply via the existing `errnox` early-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_type` value 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_lsn` covers the watermark; `-ETIMEDOUT` with ACKs withheld and `committed_lsn` unmoved; 0 after covering ACK; `n_peers <= 1` immediate 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_lsn` from 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_lsn` covers 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_tag` unmoved, no restart requested; with acks → success, tag advanced (4.1-4.4)
|
|
- Reply-site audit (2.4): scripted grep over all `xce_reply` call 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.c` configAck 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_ms` and 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"):
|
|
1. Classification + RETAIN registration + barrier helper (no behavior change for replies yet)
|
|
2. Handler restructure sweep + fact gate admission (reply semantics switch on)
|
|
3. 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.
|