forked from lei.chen/myxce
fix(raft-apply-decoupling): bug-1419 layer 3 — close cross-primary client-retry dedup gap
jobid_range_exists() (c->jobmgr) and the in-flight registry (job.c,
per-process, only populated by this node's own handle_jobsubmit calls)
both miss a real cross-node case: a client's XCECLI_FAILOVER_CALL retry
resends the same client-generated jobid after a connection drop, and the
resend lands on a newly elected primary that never processed the original
request. If the original was already committed before the old primary
died, the new primary's own local WAL already holds it (Raft Leader
Completeness) but jobmgr hasn't applied it yet — both existing guards see
nothing, so the resend appends a second, genuinely committed WAL_JOB_SUBMIT
entry for the same jobid. A live chaotic soak reproduced this exactly
(waldump: two committed entries, same term, matching apiserverId/timestamp/
sequence, one second apart) — apply's own idempotent dedup keeps jobmgr
correct, but replay's raw WAL_JOB_SUBMIT count no longer matches jobmgr's
growth, firing a false APPLY_DIVERGENT fence and deadlocking promotion.
Fix: apply_committed_entry()'s WAL_JOB_SUBMIT case now claims the jobid in
job.c's in-flight registry (new job_wal_commit_claim/release, wrapping the
existing inflight_range_try_add/remove) for the duration of dispatch,
regardless of whether this node originated the submission or is applying a
replicated/replayed entry. A resend landing mid-dispatch on any node now
sees the same -EEXIST rejection self-originated in-flight submits already
get. Release only fires when the claim call added the entry, so a
self-originated submission's own claim/release lifecycle (unchanged) is
never double-released or stolen.
Also fixes 4 pre-existing tests that broke under the earlier checkpoint_lsn
fix (0eb1054) and were missed in that commit's regression sweep: cluster-
less fixtures where last_applied_lsn never advances now correctly get
checkpoint_lsn=0 unless the test simulates apply catch-up, matching the
new (correct) semantics. One assertion in
test_checkpoint_tail_with_no_snapshot_is_apply_clean compared Phase 1's
checkpoint_lsn against Phase 2's independently-reconstructed physical WAL
tip — that equality only held under the pre-fix current_lsn-tracking
behavior and is removed, not patched around.
Also hardens run-ha-soak.sh's start_node() with setsid+nohup: a forked/
background test-harness invocation whose session tears down mid-run was
silently killing every controller process via SIGHUP forwarding (observed
twice — deploy completes, ~1s of activity, then all 3 nodes die together
with no error logged).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VgT3n4vTv4ZjCuCTmzed1F
This commit is contained in:
parent
1b6041acef
commit
f28ce57993
10 changed files with 206 additions and 16 deletions
|
|
@ -1381,6 +1381,47 @@ static void test_jobsubmit_resubmit_after_full_resolution_rejected_no_second_job
|
|||
" PASS\n");
|
||||
}
|
||||
|
||||
/* bug-1419 layer 3: a jobid claimed by something OTHER than this node's own
|
||||
* handle_jobsubmit call -- simulating the apply pump processing a
|
||||
* WAL_JOB_SUBMIT entry received via replication/replay, never locally
|
||||
* submitted -- must still block a same-jobid submit arriving through
|
||||
* handle_jobsubmit. Before this fix, job_wal_commit_claim did not exist and
|
||||
* apply_committed_entry() never registered an in-flight claim for an
|
||||
* externally-sourced entry, so jobid_range_exists() (c->jobmgr, not yet
|
||||
* applied) and the in-flight registry (only ever populated by
|
||||
* handle_jobsubmit itself) both missed it -- exactly the cross-primary
|
||||
* retry gap a live chaotic soak reproduced (waldump showed two committed
|
||||
* WAL_JOB_SUBMIT entries for one jobid, same term, no fresh idgen collision
|
||||
* -- the same generated jobid resent by the client after a connection drop,
|
||||
* landing on a newly elected primary that never saw the original). */
|
||||
static void test_jobsubmit_rejected_when_jobid_claimed_by_foreign_commit(void)
|
||||
{
|
||||
const uint64_t jobid = 9160;
|
||||
int rc;
|
||||
|
||||
cbrp_setup(2000);
|
||||
cbrp_arrange_jobsubmit(jobid, 1);
|
||||
|
||||
/* Simulate: the apply pump is mid-dispatch on a WAL_JOB_SUBMIT entry for
|
||||
* this jobid that this node never itself submitted (e.g. replicated
|
||||
* from a prior primary that committed it before dying). */
|
||||
TEST_ASSERT_TRUE_MESSAGE(job_wal_commit_claim(jobid),
|
||||
"fixture sanity: claiming a fresh jobid must succeed");
|
||||
|
||||
rc = handle_jobsubmit(&g_hc, &g_req_ep, &g_jobsub_req);
|
||||
|
||||
job_wal_commit_release(jobid);
|
||||
cbrp_teardown();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-EEXIST, rc,
|
||||
"bug-1419 layer 3: a submit for a jobid claimed by a foreign "
|
||||
"(non-self-originated) commit must be rejected -EEXIST, closing "
|
||||
"the cross-primary retry gap");
|
||||
|
||||
printf("test_jobsubmit_rejected_when_jobid_claimed_by_foreign_commit:"
|
||||
" PASS\n");
|
||||
}
|
||||
|
||||
/* The explicit-FAILED (non-timeout) sibling of the repeated-timeout test
|
||||
* above: repeats the truncation-forced FAILED path
|
||||
* CBRP_JOBSUBMIT_REPEAT_N times in the same fixture, proving the
|
||||
|
|
@ -3347,6 +3388,7 @@ void test_commit_barrier_reply(void)
|
|||
RUN_TEST(test_jobsubmit_repeated_timeout_failures_no_phantom_jobs);
|
||||
RUN_TEST(test_jobsubmit_resubmit_before_resolution_rejected_no_dup_wal_entry);
|
||||
RUN_TEST(test_jobsubmit_resubmit_after_full_resolution_rejected_no_second_job);
|
||||
RUN_TEST(test_jobsubmit_rejected_when_jobid_claimed_by_foreign_commit);
|
||||
RUN_TEST(test_jobsubmit_repeated_truncation_forced_failures_no_phantom_jobs);
|
||||
RUN_TEST(test_jobsubmit_array_mixed_outcome_no_phantom_job_for_failed_elements);
|
||||
/* handle_jobsubmit success path via the ordered apply pump
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue