feat(config-upgrade-snapshot): remove restart/replay fallback, marker-trigger materializes alone

do_upgrade_config_with_bytes drops step 9 (wait-for-checkpoint,
restart_requested=1, cbuf_put stop signal) -- materialization now
happens entirely via task 5's apply_committed_entry WAL_CONFIG_MARKER
case on every member's own apply, no restart required. Confirmed
do_upgrade_config_with_bytes was the sole production setter of
restart_requested; cluster.c's read of it is now permanently
false-for-this-trigger, which is the intended behavior.

Adds xce_upgrade_marker_committed_hook (#ifdef XCE_TESTING, mirrors
xce_snapshot_chunk_hook's nullable-function-pointer shape), firing
right after config_commit_change succeeds -- a deterministic seam for
later leadership-change fault injection (task 8.7) at exactly the
marker-commit-to-every-member-installed window. NULL by default, true
no-op in production.

Task 7 of 19.

Requirements: 1.2, 1.3, 1.4, 2.2, 5.4, 6.3
This commit is contained in:
Rongsong Shen 2026-07-25 05:37:27 +00:00
parent 89b85e1680
commit 41af05402d
3 changed files with 135 additions and 46 deletions

View file

@ -1505,13 +1505,18 @@ static void test_upgrade_config_barrier_failure_error_reply_no_restart(void)
printf("test_upgrade_config_barrier_failure_error_reply_no_restart: PASS\n");
}
/* Success path PIN: with a full majority of covering ACKs the existing
* behavior is unchanged — status-0 reply with the upgradeConfigReply
* payload, committed_config_tag at the saved tag, restart requested.
* Both standbys pre-ack UINT64_MAX (covers any tag — the new tag is only
* known after conf_save_config hashes the re-encoded blob), so the case
* is green under both the legacy counting and the per-push arithmetic. */
static void test_upgrade_config_majority_ack_success_unchanged(void)
/* Success path PIN: with a full majority of covering ACKs, status-0 reply
* with the upgradeConfigReply payload and committed_config_tag at the saved
* tag are unchanged — but as of config-upgrade-snapshot task 7 the
* restart/replay fallback is retired: a successful upgrade must NOT set
* restart_requested (Req 1.2/1.3/1.4/2.2/5.4). Materialization is now
* driven entirely by the marker-apply trigger (apply_committed_entry's
* WAL_CONFIG_MARKER case, persist.c, task 5), decoupled from this
* request-handling path. Both standbys pre-ack UINT64_MAX (covers any tag
* — the new tag is only known after conf_save_config hashes the re-encoded
* blob), so the case is green under both the legacy counting and the
* per-push arithmetic. */
static void test_upgrade_config_majority_ack_success_no_restart(void)
{
uint8_t *blob;
size_t blen = 0;
@ -1530,7 +1535,7 @@ static void test_upgrade_config_majority_ack_success_unchanged(void)
/* Act */
rc = handle_upgrade_config(&g_cb5_c, &g_cb5_req_ep, &g_cb5_req);
/* Assert: existing success reply + restart behavior unchanged */
/* Assert: existing success reply unchanged; restart is NOT requested */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
"a majority-acked upgrade must succeed");
cb5_scan_replies(&g_cb5_req_ep, &obs);
@ -1547,12 +1552,98 @@ static void test_upgrade_config_majority_ack_success_unchanged(void)
atomic_load_explicit(&g_cb5_c.ha.committed_config_tag,
memory_order_acquire),
"a majority-acked config must be committed at the saved tag (Req 4.1)");
TEST_ASSERT_EQUAL_INT_MESSAGE(1, atomic_load(&g_cb5_c.restart_requested),
"a successful upgrade must request the activation restart");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, atomic_load(&g_cb5_c.restart_requested),
"a successful upgrade must NOT request a restart -- materialization"
" happens via the marker-apply trigger alone (task 7, Req 1.2-1.4)");
free(blob);
cb5_teardown();
printf("test_upgrade_config_majority_ack_success_unchanged: PASS\n");
printf("test_upgrade_config_majority_ack_success_no_restart: PASS\n");
}
/* ── config-upgrade-snapshot task 7: marker-committed test hook ─────────── */
/* Captures each xce_upgrade_marker_committed_hook firing: the call count,
* the version_tag observed, and a snapshot of ha.committed_config_tag taken
* AT FIRE TIME -- proving the hook runs strictly after config_commit_change()
* has already advanced the committed tag (i.e. after the marker reached the
* committed prefix), not before. */
static int g_cb5_hook_calls;
static uint64_t g_cb5_hook_last_tag;
static uint64_t g_cb5_hook_committed_tag_at_fire;
static int cb5_marker_committed_hook(cluster_t *c, uint64_t version_tag)
{
g_cb5_hook_calls++;
g_cb5_hook_last_tag = version_tag;
g_cb5_hook_committed_tag_at_fire =
atomic_load_explicit(&c->ha.committed_config_tag, memory_order_acquire);
return 0;
}
/* The new #ifdef XCE_TESTING seam (config-upgrade-snapshot task 7,
* xce_upgrade_marker_committed_hook in config_repl.c) must fire exactly
* once per successful upgrade, right after config_commit_change() succeeds
* -- i.e. after the configuration-change marker has reached the committed
* prefix -- and must leave every other observable behavior (reply,
* committed tag, no restart) exactly as the NULL-hook case already asserts,
* proving the seam is a true no-op in production (hook defaults to NULL). */
static void test_upgrade_config_marker_committed_hook_fires_once_after_commit(void)
{
uint8_t *blob;
size_t blen = 0;
uint8_t sha[32];
int rc;
cb5_reply_obs_t obs = { 0 };
extern int (*xce_upgrade_marker_committed_hook)(cluster_t *, uint64_t);
/* Arrange: full covering pre-ack from both standbys (same fixture as
* the no-restart success-path test) */
cb5_setup_upgrade();
blob = cb5_make_config_blob(&blen);
SHA256(blob, blen, sha);
cb5_arrange_upgrade_req(blob, blen, sha);
repl_on_config_ack(&g_cb5_c.persist.repl, &g_cb5_sb_ep[0], UINT64_MAX);
repl_on_config_ack(&g_cb5_c.persist.repl, &g_cb5_sb_ep[1], UINT64_MAX);
g_cb5_hook_calls = 0;
g_cb5_hook_last_tag = 0;
g_cb5_hook_committed_tag_at_fire = 0;
xce_upgrade_marker_committed_hook = cb5_marker_committed_hook;
/* Act */
rc = handle_upgrade_config(&g_cb5_c, &g_cb5_req_ep, &g_cb5_req);
/* Assert: fired exactly once, with the just-committed tag, and only
* after ha.committed_config_tag already reflects that same tag. */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "a majority-acked upgrade must succeed");
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_cb5_hook_calls,
"the marker-committed test hook must fire exactly once per"
" successful upgrade (task 7)");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(g_cb5_myconf.config_version_tag,
g_cb5_hook_last_tag,
"the hook must observe the marker's own version tag");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(g_cb5_myconf.config_version_tag,
g_cb5_hook_committed_tag_at_fire,
"the hook must fire AFTER config_commit_change already advanced"
" committed_config_tag, not before");
/* The rest of the observable behavior must be identical to the
* NULL-hook (production default) case -- the hook is side-effect-free
* beyond invoking the test callback itself. */
cb5_scan_replies(&g_cb5_req_ep, &obs);
TEST_ASSERT_EQUAL_INT_MESSAGE(1, obs.n_replies,
"the requester must still receive exactly one reply with the hook set");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, obs.last_status,
"the success ACK must still carry status 0 with the hook set");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, atomic_load(&g_cb5_c.restart_requested),
"the hook firing must not resurrect the retired restart request");
xce_upgrade_marker_committed_hook = NULL;
free(blob);
cb5_teardown();
printf("test_upgrade_config_marker_committed_hook_fires_once_after_commit:"
" PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
@ -2351,7 +2442,8 @@ void test_commit_barrier_rollout(void)
RUN_TEST(test_push_config_withheld_acks_returns_etimedout);
RUN_TEST(test_push_config_majority_acks_returns_zero_and_commits);
RUN_TEST(test_upgrade_config_barrier_failure_error_reply_no_restart);
RUN_TEST(test_upgrade_config_majority_ack_success_unchanged);
RUN_TEST(test_upgrade_config_majority_ack_success_no_restart);
RUN_TEST(test_upgrade_config_marker_committed_hook_fires_once_after_commit);
/* Task 5.2 remediation r1 — production wire-path ACK ingestion
* (completes the unlanded half of ha-config-bootstrap task 8.1) */
RUN_TEST(test_config_ack_wire_path_completes_barrier);