forked from lei.chen/myxce
feat(config-upgrade-snapshot): gate primary serving and promotion on the activation fence
Wires task 6.1's upgrade_materialization_verify/upgrade_materialization_fenced into ha_dispatch_gate's CTRL_PRIMARY branch (sequential block alongside, not merged into, the existing config_serve_fenced check -- same replication/read-only opcode allowlist, refuses state-changing ops with -XCE_ERR_NOT_READY while fenced) and the promotion gate (raft_cb_become_leader refuses to become CTRL_PRIMARY while fenced, introduces no new WAL/log-completeness eligibility mechanism, never self-terminates). Adds a 4-function self-triggering retry trampoline (upgrade_materialization_retry_fn/_vtask_entry/_delaymgr_trampoline/ schedule_upgrade_materialization_retry) mirroring config_activate_retry_fn's exact shape, hooked into ha_cb_commit_advanced_stub alongside the existing config-byte retry. Fixes a real bug caught on review: the fence had no production path that could ever engage it from its zero-initialized default -- every setter call site was gated on the fence already being set (closed loop), making the new gate checks permanently dead code. Fixed with two unconditional upgrade_materialization_verify() call sites: persist.c's apply_committed_entry WAL_CONFIG_MARKER case (right after persist_checkpoint_marker_triggered returns, regardless of its rc) and myapp.c's boot sequence (right after config_activate_committed, for Req 5.2 crash/restart resume). New test drives the fence through the real production trigger (a forced -EBUSY checkpoint miss on a real marker commit+apply), not a direct verify() call, proving the end-to-end path actually works. Task 6.2 of 19. Requirements: 3.1, 3.2, 3.3, 3.4, 4.3, 5.5
This commit is contained in:
parent
7b507156a0
commit
89b85e1680
6 changed files with 805 additions and 7 deletions
|
|
@ -288,6 +288,124 @@ int upgrade_materialization_fenced(const cluster_t *c)
|
|||
memory_order_acquire) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ── config-upgrade-snapshot task 6.2: Activation Fence self-triggering retry ──
|
||||
* Mirrors config_schedule_activate_retry's exact four-function shape
|
||||
* (config_repl.c:409-507) for the upgrade_materialization_fence instead of
|
||||
* config_serve_fence: a one-shot NON-BLOCKING delaymgr task offloads the
|
||||
* actual blocking re-verify work (upgrade_materialization_verify's disk
|
||||
* reads) onto a VTASK_LIVE_BLOCK vtask, so the delaymgr thread itself never
|
||||
* blocks (ha-async-io.md thread-role contract). */
|
||||
|
||||
/* upgrade_materialization_retry_fn: re-runs upgrade_materialization_verify so
|
||||
* a member whose materialization was incomplete or hash-mismatched re-checks
|
||||
* and clears the fence once conditions allow. Does the actual blocking disk
|
||||
* I/O; callers must invoke it off the delaymgr thread (see
|
||||
* upgrade_materialization_retry_vtask_entry /
|
||||
* schedule_upgrade_materialization_retry below). Public so tests can drive it
|
||||
* directly without a vtask pool — mirrors config_activate_retry_fn. */
|
||||
int upgrade_materialization_retry_fn(void *arg)
|
||||
{
|
||||
cluster_t *c = (cluster_t *)arg;
|
||||
if (!c)
|
||||
return -EINVAL;
|
||||
if (!upgrade_materialization_fenced(c))
|
||||
return 0; /* already cleared by another path — nothing to retry */
|
||||
|
||||
int rc = upgrade_materialization_verify(c);
|
||||
if (rc == 0)
|
||||
INFO("upgrade materialization retry: fence CLEARED — this member's "
|
||||
"own materialization for the committed config marker now "
|
||||
"verified");
|
||||
else if (rc == -EAGAIN)
|
||||
TRACE("upgrade materialization retry: still fenced (materialization "
|
||||
"incomplete or hash-mismatched) — will retry on the next "
|
||||
"commit advance");
|
||||
else
|
||||
WARN("upgrade materialization retry: upgrade_materialization_verify "
|
||||
"rc=%d", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* upgrade_materialization_retry_vtask_entry: vtask entry wrapping
|
||||
* upgrade_materialization_retry_fn so its blocking disk I/O runs on a vtask
|
||||
* worker, never inline on the single delaymgr thread. Mirrors
|
||||
* config_activate_retry_vtask_entry. */
|
||||
static int upgrade_materialization_retry_vtask_entry(vtask_t *task)
|
||||
{
|
||||
cluster_t *c = (cluster_t *)task->data->u.generic;
|
||||
upgrade_materialization_retry_fn(c);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* upgrade_materialization_retry_delaymgr_trampoline: the delaymgr task body.
|
||||
* Delaymgr must only trigger + enqueue, never block — dispatch the actual
|
||||
* retry work to a vtask (VTASK_LIVE_BLOCK, the same class
|
||||
* config_activate_retry_delaymgr_trampoline uses) and return immediately.
|
||||
* Fails loud and runs inline (stay alive, never silently drop a scheduled
|
||||
* retry) if the pool is unavailable. Mirrors
|
||||
* config_activate_retry_delaymgr_trampoline. */
|
||||
static int upgrade_materialization_retry_delaymgr_trampoline(void *arg)
|
||||
{
|
||||
cluster_t *c = (cluster_t *)arg;
|
||||
if (!c)
|
||||
return -EINVAL;
|
||||
|
||||
vtaskmgr_t *pool = &c->vtaskmgr;
|
||||
vtask_data_t *data = calloc(1, sizeof(*data));
|
||||
vtask_t *task = vtask_alloc();
|
||||
|
||||
if (data == NULL || task == NULL) {
|
||||
WARN("upgrade materialization retry: vtask alloc failed — running "
|
||||
"inline (stay alive)");
|
||||
free(data);
|
||||
free(task);
|
||||
return upgrade_materialization_retry_fn(c);
|
||||
}
|
||||
|
||||
data->vtype = VTASK_GENERIC;
|
||||
data->u.generic = c;
|
||||
vtask_init(task, (vtask_func_t)upgrade_materialization_retry_vtask_entry,
|
||||
data);
|
||||
|
||||
if (vtask_enq_classified(pool, task, VTASK_LIVE_BLOCK) < 0) {
|
||||
WARN("upgrade materialization retry: vtask enqueue failed — running "
|
||||
"inline (stay alive)");
|
||||
free(data);
|
||||
free(task);
|
||||
return upgrade_materialization_retry_fn(c);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* schedule_upgrade_materialization_retry: enqueue the retry trampoline on the
|
||||
* delaymgr (NON-BLOCKING — delay_task_ms only does ordq_put; the trampoline
|
||||
* itself offloads the blocking work to a vtask, see above). Called from the
|
||||
* commit-advance path (iomgr ACK thread) so the retry's disk I/O runs OFF
|
||||
* that thread (ha-async-io invariant). No-op when the member is NOT
|
||||
* upgrade-materialization fenced (steady state schedules nothing) or when
|
||||
* the delaymgr is not running (the unit-test fixture path, which never inits
|
||||
* delaymgr — avoids leaking a task into an un-drained queue). Mirrors
|
||||
* config_schedule_activate_retry exactly. Returns 0 on enqueue/no-op,
|
||||
* negative on enqueue failure. */
|
||||
int schedule_upgrade_materialization_retry(cluster_t *c)
|
||||
{
|
||||
if (!c)
|
||||
return -EINVAL;
|
||||
if (!upgrade_materialization_fenced(c))
|
||||
return 0; /* steady state: nothing to retry, schedule nothing */
|
||||
if (!c->delaymgr.running)
|
||||
return 0; /* delaymgr not running (test fixture) — caller drives directly */
|
||||
|
||||
/* One-shot, minimal delay: run promptly but off the iomgr ACK thread. */
|
||||
int rc = delay_task_ms(&c->delaymgr, 1, 1,
|
||||
(delay_task_entry_fn_t)upgrade_materialization_retry_delaymgr_trampoline,
|
||||
c);
|
||||
if (rc < 0)
|
||||
WARN("upgrade materialization retry: delay_task_ms failed: %d — "
|
||||
"fence will clear on a later commit advance", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void on_vote_connected(endpoint_t *ep);
|
||||
static void on_pre_vote_connected(endpoint_t *ep);
|
||||
static int vote_ep_err_handler(void *ep_raw);
|
||||
|
|
@ -1622,6 +1740,35 @@ static void raft_cb_become_leader(void *ctx, uint64_t term)
|
|||
return; /* do NOT become CTRL_PRIMARY; never self-exit */
|
||||
}
|
||||
|
||||
/* ── config-upgrade-snapshot task 6.2 (Req 1.8, 3.2, 3.3, 4.3, 5.5):
|
||||
* Activation Fence promotion gate ──────────────────────────────────────
|
||||
* A member whose OWN upgrade-snapshot materialization for the current
|
||||
* committed config marker has not completed and hash-verified must not
|
||||
* become authoritative CTRL_PRIMARY — doing so would let it win an
|
||||
* election (Raft's log-freshness gate only proves it has the committed
|
||||
* MARKER, not that it has finished MATERIALIZING under it) and then
|
||||
* immediately violate the serving gate above the moment it starts
|
||||
* accepting requests. This is a SEPARATE, SEQUENTIAL check from the
|
||||
* config serve-fence directly above (same rationale as the dispatch-gate
|
||||
* composition: byte-mismatch and materialization-incomplete are distinct
|
||||
* conditions) — never a new promotion-eligibility mechanism for WAL/log
|
||||
* completeness (Req 3.1 is explicit that Raft's own freshness gate
|
||||
* already owns that; this fence is layered on top, purely about
|
||||
* materialization completeness). Refuse promotion and STAY ALIVE
|
||||
* (controller-never-self-terminate): the member remains non-promotable
|
||||
* and keeps retrying (schedule_upgrade_materialization_retry, driven off
|
||||
* the commit-advance path); once its own materialization completes and
|
||||
* re-verify clears the fence, a later promotion attempt passes this
|
||||
* gate. Pure atomic read (no I/O) — safe on the election path. */
|
||||
if (upgrade_materialization_fenced(c)) {
|
||||
WARN("HA: promotion REFUSED — upgrade materialization FENCED (this "
|
||||
"member's own upgrade-snapshot materialization for the "
|
||||
"committed config marker is incomplete or hash-mismatched); "
|
||||
"member stays ALIVE, non-promotable, retrying materialization "
|
||||
"(config-upgrade-snapshot Req 1.8/3.2/3.3/4.3/5.5)");
|
||||
return; /* do NOT become CTRL_PRIMARY; never self-exit */
|
||||
}
|
||||
|
||||
int is_first_boot = !ha->phase2_done;
|
||||
|
||||
/* First boot: call cluster_init_phase2 */
|
||||
|
|
@ -2379,6 +2526,20 @@ static void ha_cb_commit_advanced_stub(void *ctx, uint64_t commit_index)
|
|||
* commit advance. */
|
||||
if (config_serve_fenced(ha->raft_c))
|
||||
config_schedule_activate_retry(ha->raft_c);
|
||||
|
||||
/* config-upgrade-snapshot task 6.2 (Req 3.4, 5.5): symmetric commit-
|
||||
* advance trigger for the Activation Fence. Materialization technically
|
||||
* completes on APPLY (persist.c's WAL_CONFIG_MARKER case), which lags
|
||||
* commit under raft-apply-decoupling — this hook does not need to fire
|
||||
* at the PRECISE instant materialization becomes possible, only to
|
||||
* eventually re-check and clear the fence once conditions allow (Req
|
||||
* 3.4/5.5's "keep retrying" framing, not a precise event-driven
|
||||
* guarantee). Commit advancing is a reasonable, precedented "something
|
||||
* changed, worth re-checking" signal, and schedule_upgrade_materialization_
|
||||
* retry itself is a no-op when unfenced, so this adds no busy-spin in the
|
||||
* steady state. */
|
||||
if (upgrade_materialization_fenced(ha->raft_c))
|
||||
schedule_upgrade_materialization_retry(ha->raft_c);
|
||||
}
|
||||
|
||||
static const raft_callbacks_t s_real_cbs = {
|
||||
|
|
@ -4031,6 +4192,48 @@ int ha_dispatch_gate(ha_state_t *ha, int opcode)
|
|||
return -XCE_ERR_NOT_READY;
|
||||
}
|
||||
}
|
||||
/* config-upgrade-snapshot task 6.2 (Req 3.2, 3.3, 5.5): the Activation
|
||||
* Fence is a SEPARATE, SEQUENTIAL check composed alongside the
|
||||
* config-byte serve-fence above, not merged into it — the two
|
||||
* conditions are logically distinct (byte-mismatch vs. this member's
|
||||
* own materialization-incomplete) per design.md's Activation Fence
|
||||
* component. Checked AFTER config_serve_fenced deliberately: byte
|
||||
* delivery is the earlier step (the bytes must exist locally before
|
||||
* this member's own checkpoint can even attempt to materialize
|
||||
* against them), so checking byte-fence first surfaces the more
|
||||
* fundamental blocker first in log/error ordering — but either fence
|
||||
* alone is sufficient to refuse a state-changing op, so the final
|
||||
* admit/refuse outcome does not depend on this ordering. Same
|
||||
* allowlist as the byte-fence: replication + read-only/status ops
|
||||
* stay admitted (they let materialization proceed and re-verify);
|
||||
* everything else is refused with a loud non-serving error while the
|
||||
* member STAYS ALIVE and keeps retrying (controller-never-self-
|
||||
* terminate). */
|
||||
if (upgrade_materialization_fenced(ha->raft_c)) {
|
||||
switch (opcode) {
|
||||
/* Replication ops — let commit/apply advance + re-verify */
|
||||
case OPCode_heartbeat:
|
||||
case OPCode_replConfig:
|
||||
case OPCode_replConfigPush:
|
||||
case OPCode_upgradeConfigAck:
|
||||
case OPCode_hello:
|
||||
case OPCode_walAck:
|
||||
case OPCode_replEntry:
|
||||
case OPCode_replAck:
|
||||
case OPCode_replCatchUpDone:
|
||||
case OPCode_walCatchupEntry:
|
||||
case OPCode_snapshotTransfer:
|
||||
/* Read-only queries / status — answerable while fenced */
|
||||
case OPCode_get:
|
||||
case OPCode_probeConfigTag:
|
||||
case OPCode_ctrlstatus:
|
||||
break; /* fall through to the quorum check below */
|
||||
default:
|
||||
/* State-changing op while this member's own upgrade-snapshot
|
||||
* materialization is incomplete/unverified: refuse. */
|
||||
return -XCE_ERR_NOT_READY;
|
||||
}
|
||||
}
|
||||
/* State-changing operations require majority so committed entries
|
||||
* are durable across failover. Read-only queries and replication
|
||||
* ops are always allowed. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue