feat(config-upgrade-snapshot): materialize upgrade snapshot on config-marker apply
Adds an explicit case WAL_CONFIG_MARKER to apply_committed_entry, replacing its no-op treatment, that forces a checkpoint at exactly the marker's own (term, lsn) via two new helpers: persist_checkpoint_write_locked(p, checkpoint_lsn) -- the checkpoint core (pin lsn, scan config ref, write_snapshot, trim, rotate) taking an explicit LSN and never acquiring apply_pump_lock itself persist_checkpoint_marker_triggered(p, lsn) -- shares the existing checkpoint_in_progress CAS guard, calls the above with the marker's own lsn This fixes two hazards design.md's "call persist_checkpoint() directly" text didn't account for: (1) apply_committed_entry's live-pump caller (persist_apply_pump_run, used by both primary and standby online-apply) already holds apply_pump_lock across the dispatch call -- a direct persist_checkpoint() call would self-deadlock on that mutex; (2) last_applied_lsn/replay position only advance AFTER apply_committed_entry returns at both call sites, so pinning checkpoint_lsn from last_applied_lsn inside the marker case would label the checkpoint with the previous entry's LSN, not the marker's own -- fixed by threading the marker's own lsn parameter through explicitly. A -EBUSY outcome (a concurrent periodic checkpoint mid-flight) is logged at TRACE and treated as expected/self-resolving, never retried. The new case has no effect on entity interpretation/re-homing (Req 1.8). New test_upgrade_snapshot_apply.c drives WAL_CONFIG_MARKER through the REAL production call paths (persist_apply_pump_run and boot replay via persist_recover), not a bare apply_committed_entry() call, so the fix is actually proven against the lock-holding context rather than assumed. Also fixes a genuine regression in test_config_repl.c surfaced by WAL_CONFIG_MARKER apply having a real side effect for the first time (bug-1466): an fsync-worker-thread race (t42_wait_applied polling helper, mirrors test_apply_pump.c's tap_wait_applied) and one test call site trusting an unreliable raft-derived LSN. Surfaces (but does not fix, out of this spec's boundary) a pre-existing ha-term-safety/raft-log-replication gap: persist_write_checkpoint_marker bypasses ha_primary_log_append, breaking its documented current_lsn==log_len+1 invariant once a checkpoint interleaves with primary appends -- log_len feeds real RequestVote freshness comparisons, not advisory-only as first assumed; severity corrected in bug-1466. Task 5 of 19. Requirements: 1.1, 1.5, 1.8, 4.4, 5.2, 5.3
This commit is contained in:
parent
00cd26a927
commit
1da26b043e
7 changed files with 938 additions and 23 deletions
|
|
@ -2678,6 +2678,51 @@ static void schedule_checkpoint(persist_t* persist)
|
|||
checkpoint_task_fn, persist);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Checkpoint core (shared by persist_checkpoint and the marker-apply */
|
||||
/* trigger, config-upgrade-snapshot task 5) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* persist_checkpoint_write_locked: pin checkpoint_lsn to the EXPLICIT
|
||||
* `checkpoint_lsn` given (never re-derived internally from
|
||||
* last_applied_lsn), capture the committed-config reference, and serialize
|
||||
* live state to a snapshot file.
|
||||
*
|
||||
* Caller MUST already hold apply_pump_lock (or otherwise guarantee no
|
||||
* concurrent apply-pump walk can be racing this call, e.g. the
|
||||
* single-threaded boot-replay window persist_recover() already documents
|
||||
* elsewhere in this file) -- this function never locks or unlocks
|
||||
* apply_pump_lock itself, so it is safe to call both from a context that
|
||||
* already holds it (the live apply pump) and from a context where no lock
|
||||
* is needed at all (boot replay).
|
||||
*
|
||||
* The explicit-lsn parameter exists because the two callers need two
|
||||
* different values at two different moments:
|
||||
* - persist_checkpoint() (periodic/explicit trigger): last_applied_lsn,
|
||||
* read fresh by the caller immediately before this call.
|
||||
* - the marker-apply trigger (apply_committed_entry's WAL_CONFIG_MARKER
|
||||
* case): the marker's OWN lsn -- last_applied_lsn is NOT yet advanced
|
||||
* to the marker's position at the moment that case runs (both live
|
||||
* apply and replay advance it strictly AFTER dispatching this call),
|
||||
* so reading last_applied_lsn from inside that case would mislabel the
|
||||
* checkpoint with the marker's PREDECESSOR's lsn, not its own. */
|
||||
static int persist_checkpoint_write_locked(persist_t* p,
|
||||
uint64_t checkpoint_lsn)
|
||||
{
|
||||
p->checkpoint_lsn = checkpoint_lsn;
|
||||
|
||||
/* Capture the latest committed config-marker reference BEFORE trimming
|
||||
* (ha-term-safety task 4.2 r1): trim_wal_segments (run by both callers,
|
||||
* after this function returns) can unlink the segment holding a
|
||||
* committed WAL_CONFIG_MARKER, so the derived committed config must be
|
||||
* cached durably (into the sentinel) before that happens. */
|
||||
scan_latest_committed_config_ref(p);
|
||||
|
||||
/* Serialize in-memory state to a snapshot file (P0: no live kvmgrs —
|
||||
* writes sentinel header + cached config ref). */
|
||||
return write_snapshot(p);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Public: persist_checkpoint */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -2714,18 +2759,8 @@ int persist_checkpoint(persist_t* p)
|
|||
* the label ever claiming more than jobmgr actually holds. */
|
||||
pthread_mutex_lock(&p->apply_pump_lock);
|
||||
|
||||
p->checkpoint_lsn = atomic_load_explicit(&p->last_applied_lsn,
|
||||
memory_order_acquire);
|
||||
|
||||
/* 3a. Capture the latest committed config-marker reference BEFORE trimming
|
||||
* (ha-term-safety task 4.2 r1): the trim below can unlink the segment
|
||||
* holding a committed WAL_CONFIG_MARKER, so the derived committed config
|
||||
* must be cached durably (into the sentinel) before that happens. */
|
||||
scan_latest_committed_config_ref(p);
|
||||
|
||||
/* 3b. Serialize in-memory state to a snapshot file
|
||||
* (P0: no live kvmgrs — writes sentinel header + cached config ref) */
|
||||
rc = write_snapshot(p);
|
||||
rc = persist_checkpoint_write_locked(
|
||||
p, atomic_load_explicit(&p->last_applied_lsn, memory_order_acquire));
|
||||
|
||||
pthread_mutex_unlock(&p->apply_pump_lock);
|
||||
|
||||
|
|
@ -2743,6 +2778,61 @@ done:
|
|||
return rc;
|
||||
}
|
||||
|
||||
/* persist_checkpoint_marker_triggered (config-upgrade-snapshot task 5, Req
|
||||
* 1.1, 1.5, 1.8, 4.4, 5.2, 5.3): force an immediate checkpoint at exactly
|
||||
* `marker_lsn` -- the configuration-change marker's OWN lsn -- called from
|
||||
* apply_committed_entry's WAL_CONFIG_MARKER case.
|
||||
*
|
||||
* Unlike persist_checkpoint(), this function NEVER locks apply_pump_lock:
|
||||
* it relies entirely on the caller's own locking context, because the two
|
||||
* production call sites of apply_committed_entry() differ in that context
|
||||
* and this function must be correct under BOTH without deadlocking under
|
||||
* either:
|
||||
* - persist_apply_pump_run() (live online-apply, both primary and
|
||||
* standby route through this) holds apply_pump_lock across its ENTIRE
|
||||
* per-LSN dispatch loop, including the call into apply_committed_entry
|
||||
* that reaches this function. Re-locking that same, non-recursive
|
||||
* mutex here would self-deadlock -- so this function must NOT lock it;
|
||||
* it is already held by the caller for the whole duration of this call.
|
||||
* - replay_wal() (boot/restart replay, persist_recover()'s sole caller)
|
||||
* does NOT hold apply_pump_lock during its per-entry dispatch loop --
|
||||
* by design, since persist_recover() runs before the apply pump is
|
||||
* ever kicked and nothing else can be racing it (see persist_recover's
|
||||
* own doc comment on its brief, unrelated apply_pump_lock use for
|
||||
* cache invalidation). No lock is needed here either, for the same
|
||||
* reason.
|
||||
* Both call sites are internal to persist.c and fully controlled, so this
|
||||
* documented precondition (caller already serializes, one way or another)
|
||||
* is safe to rely on rather than adding a redundant/ambiguous lock here.
|
||||
*
|
||||
* Shares checkpoint_in_progress -- the SAME single-in-flight CAS guard
|
||||
* persist_checkpoint() uses -- so a concurrent WAL-threshold-triggered
|
||||
* periodic checkpoint and this marker-triggered one can never both
|
||||
* proceed at once (Req 5.3). Returns -EBUSY when that guard is already
|
||||
* held by someone else; the caller (apply_committed_entry) treats that as
|
||||
* an expected, self-resolving condition, not a failure (Req 4.4) -- the
|
||||
* in-flight checkpoint (whichever triggered it) still completes and
|
||||
* either it or a later checkpoint picks up this marker's configuration
|
||||
* (every checkpoint embeds the currently active configuration, task 2.3). */
|
||||
static int persist_checkpoint_marker_triggered(persist_t* p,
|
||||
uint64_t marker_lsn)
|
||||
{
|
||||
if (atomic_exchange(&p->checkpoint_in_progress, 1) != 0)
|
||||
return -EBUSY;
|
||||
|
||||
int rc = persist_write_checkpoint_marker(p);
|
||||
if (rc == 0)
|
||||
rc = persist_checkpoint_write_locked(p, marker_lsn);
|
||||
|
||||
if (rc == 0) {
|
||||
trim_wal_segments(p);
|
||||
rotate_snapshots(p);
|
||||
}
|
||||
|
||||
atomic_store(&p->checkpoint_in_progress, 0);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Recovery internals */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -3641,10 +3731,79 @@ int apply_committed_entry(persist_t* p, uint32_t wal_type, uint64_t lsn,
|
|||
return 0;
|
||||
}
|
||||
|
||||
case WAL_CONFIG_MARKER: {
|
||||
/* config-upgrade-snapshot task 5 (Req 1.1, 1.5, 1.8): force an
|
||||
* immediate, deterministic checkpoint at exactly this marker's own
|
||||
* (term, lsn) boundary, materializing the upgrade snapshot the
|
||||
* moment every member applies the marker -- primary online-apply,
|
||||
* standby online-apply, and boot/restart replay alike, since this
|
||||
* one unified apply seam is exactly what all three route through
|
||||
* (design.md "Architecture" / "Key simplification found during
|
||||
* design": no new leader-specific "resume" logic is needed either,
|
||||
* since a newly-elected leader satisfies this the same way, by
|
||||
* continuing to apply its own committed log).
|
||||
*
|
||||
* Deliberately does NOT touch entity interpretation/re-homing
|
||||
* (lost_and_found etc.) -- Req 1.8 requires apply and activation to
|
||||
* stay separate steps; this case's only effect is materialization,
|
||||
* identical in kind to any other checkpoint.
|
||||
*
|
||||
* persist_checkpoint_marker_triggered() (not a bare
|
||||
* persist_checkpoint() call) is required here to avoid two real
|
||||
* hazards a naive direct call would hit:
|
||||
* 1. Lock reentrancy: persist_apply_pump_run() (the live pump's
|
||||
* call site for online-apply, both primary and standby) holds
|
||||
* apply_pump_lock across this entire dispatch call;
|
||||
* persist_checkpoint()'s own body re-acquires that same,
|
||||
* non-recursive mutex, which would self-deadlock.
|
||||
* persist_checkpoint_marker_triggered() never locks
|
||||
* apply_pump_lock itself -- see its own doc comment for why
|
||||
* that is safe under both production call sites.
|
||||
* 2. Stale checkpoint_lsn: both live-apply and replay advance
|
||||
* last_applied_lsn AFTER this dispatch call returns, never
|
||||
* before (persist_apply_pump_run / replay_wal), so reading
|
||||
* last_applied_lsn from inside this case would capture the
|
||||
* PRECEDING entry's lsn, not the marker's own, mislabeling the
|
||||
* checkpoint. `lsn` (this function's own parameter) is
|
||||
* already the marker's exact position, so it is passed
|
||||
* through explicitly instead of re-derived. */
|
||||
int rc = persist_checkpoint_marker_triggered(p, lsn);
|
||||
if (rc == -EBUSY) {
|
||||
/* Expected, self-resolving (Req 4.4/5.3): a concurrent
|
||||
* WAL-threshold-triggered or earlier marker-triggered
|
||||
* checkpoint already holds the shared single-in-flight guard
|
||||
* (checkpoint_in_progress). That in-flight checkpoint
|
||||
* completes on its own; this marker's own configuration is
|
||||
* still picked up by it or a later checkpoint (every
|
||||
* checkpoint embeds the currently active configuration, task
|
||||
* 2.3) -- not an error, no retry loop here. */
|
||||
TRACE("apply_committed_entry: WAL_CONFIG_MARKER lsn=%" PRIu64
|
||||
" checkpoint already in progress -- skipping, "
|
||||
"self-resolving", lsn);
|
||||
return 0;
|
||||
}
|
||||
if (rc != 0) {
|
||||
/* A genuine checkpoint failure (I/O error, etc.) -- propagate
|
||||
* so the caller's existing halt-and-retry-from-same-position
|
||||
* handling engages (persist_apply_pump_run / replay_wal),
|
||||
* exactly mirroring every other negative-return case in this
|
||||
* dispatch. last_applied_lsn is NOT advanced past this marker,
|
||||
* so the next kick/retry re-attempts the checkpoint fresh
|
||||
* (Req 5.2 crash-safety: resume materialization from the
|
||||
* marker). */
|
||||
ERROR("apply_committed_entry: WAL_CONFIG_MARKER lsn=%" PRIu64
|
||||
" forced checkpoint failed (rc=%d)", lsn, rc);
|
||||
return rc;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
default:
|
||||
/* WAL_NOOP / WAL_CONFIG_MARKER / WAL_CHECKPOINT / unknown — no
|
||||
* entity effect at this seam, no hook (unchanged from
|
||||
* apply_wal_entry's existing treatment of these types). */
|
||||
/* WAL_NOOP / WAL_CHECKPOINT / unknown — no entity effect at this
|
||||
* seam, no hook (unchanged from apply_wal_entry's existing
|
||||
* treatment of these types). WAL_CONFIG_MARKER has its own explicit
|
||||
* case above (config-upgrade-snapshot task 5) and no longer falls
|
||||
* through to here. */
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue