fix(raft-apply-decoupling): bug-1419 close resubmit-dedup gap left by apply-after-commit deferral
jobid_range_exists() only checks c->jobmgr, but task 3.1 deliberately defers job construction until after commit -- so a submission that is durably WAL-appended and awaiting its commit outcome is invisible to that check. A client-side retry (XCECLI_FAILOVER_CALL resending the same jobid after a transport error or failover mid-flight) sails past the guard and appends a second, non-identical WAL_JOB_SUBMIT entry for the same jobid -- confirmed via waldump on a live chaotic-soak node (duplicate submits, same jobid, differing subtime/CRC) -- which can trip persist_recover()'s post-replay apply-correctness check into a genuine APPLY_DIVERGENT verdict. Adds a process-lifetime in-flight-jobid-range registry in job.c that handle_jobsubmit atomically check-and-claims immediately after the existing jobid_range_exists check, releasing from jobsubmit_async_ctx_release (the single choke point already shared by the commit-continuation, apply-complete, and OOM early-out paths). A resubmit for a still-in-flight jobid now gets -EEXIST with no second WAL append. Adds a matching XCE_TESTING reset seam (declared in cluster.h) so a test that deliberately leaves a submission's commit continuation unresolved through teardown doesn't leak its jobid range into a later test reusing the same range -- commit_engine_fini frees pending continuations without invoking their callback. Regression test added to test_commit_barrier_reply.c (RED->GREEN confirmed). Full 78-group controller_unit_test sweep: 782 tests, 0 failures. This fixes only the WAL duplicate-submit mechanism (layer 1 of bug-1419). The deeper PreVote-freshness/apply-divergence liveness deadlock (layer 2 -- no recovery path when the only fresh-enough node is apply-divergent-fenced) remains open and needs its own design discussion; buglog.json updated accordingly.
This commit is contained in:
parent
9b173c9a5e
commit
fe4352fb36
4 changed files with 272 additions and 13 deletions
|
|
@ -431,6 +431,12 @@ static void cbrp_teardown(void)
|
|||
|
||||
cbrp_scan_replies(&g_req_ep, &drain); /* free any leftover packets */
|
||||
repl_broadcast_send_hook = NULL;
|
||||
/* bug-1419: commit_engine_fini frees any still-pending continuation
|
||||
* without invoking its callback, so a test that deliberately leaves a
|
||||
* submission unresolved (no ack/timeout/truncation) leaks its jobid
|
||||
* range in job.c's process-lifetime in-flight registry. Clear it here
|
||||
* so it never outlives this fixture. */
|
||||
inflight_submits_reset_for_test();
|
||||
commit_engine_fini(&g_hce);
|
||||
persist_stop(&g_hc.persist);
|
||||
persist_fini(&g_hc.persist);
|
||||
|
|
@ -1224,6 +1230,81 @@ static void test_jobsubmit_repeated_timeout_failures_no_phantom_jobs(void)
|
|||
printf("test_jobsubmit_repeated_timeout_failures_no_phantom_jobs: PASS\n");
|
||||
}
|
||||
|
||||
/* bug-1419: a client retry that resends the SAME jobid before it has seen
|
||||
* the first attempt's outcome must be rejected as a duplicate, not append
|
||||
* a second WAL_JOB_SUBMIT entry.
|
||||
*
|
||||
* Root cause this proves fixed: jobid_range_exists()'s c->jobmgr check is
|
||||
* airtight against a genuinely completed prior submission (the job is
|
||||
* there), but raft-apply-decoupling task 3.1 deliberately defers job
|
||||
* construction until AFTER commit -- so a submission that is durably
|
||||
* appended and awaiting its commit outcome does NOT yet exist in
|
||||
* c->jobmgr, and jobid_range_exists() alone cannot see it. libxce's
|
||||
* client-side failover retry wrapper (XCECLI_FAILOVER_CALL,
|
||||
* client_failover.c) resends the identical jobid on a transport error or
|
||||
* failover-mid-flight, exactly the shape this test drives directly against
|
||||
* the handler.
|
||||
*
|
||||
* Withholds ACKs (mirrors cbrp_drive_withheld) so the first call's commit
|
||||
* stays genuinely unresolved when the second call arrives -- the real
|
||||
* race window, not a timing approximation. */
|
||||
static void test_jobsubmit_resubmit_before_resolution_rejected_no_dup_wal_entry(void)
|
||||
{
|
||||
const uint64_t jobid = 9139;
|
||||
uint64_t lsn_before, lsn_before_second, watermark_first, watermark_second;
|
||||
int rc_second;
|
||||
|
||||
cbrp_setup(2000); /* generous: must never fire -- this test resolves
|
||||
* via explicit teardown, not the timeout sweep */
|
||||
|
||||
cbrp_arrange_jobsubmit(jobid, 1);
|
||||
|
||||
pthread_mutex_lock(&g_hc.persist.lock);
|
||||
lsn_before = g_hc.persist.current_lsn;
|
||||
pthread_mutex_unlock(&g_hc.persist.lock);
|
||||
|
||||
/* First attempt: durably appends and registers its commit continuation,
|
||||
* then returns immediately (task 3.2 -- handle_jobsubmit never blocks).
|
||||
* No ACK is ever delivered, so its outcome stays unresolved for the
|
||||
* rest of this test. */
|
||||
g_op_arg.fn = handle_jobsubmit;
|
||||
g_op_arg.req = &g_jobsub_req;
|
||||
g_op_arg.rc = -9999;
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_create(&g_op_tid, NULL,
|
||||
cbrp_op_thread_fn, &g_op_arg));
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_join(g_op_tid, NULL));
|
||||
|
||||
watermark_first = cbrp_poll_append(&g_hc.persist, lsn_before, 1000);
|
||||
TEST_ASSERT_TRUE_MESSAGE(watermark_first > 0,
|
||||
"the first attempt must append its WAL entry");
|
||||
|
||||
/* Second attempt: the retry, same jobid, while the first is still
|
||||
* in flight (no ACK delivered, no timeout elapsed). Called directly --
|
||||
* handle_jobsubmit never blocks, so no second thread is needed. */
|
||||
cbrp_arrange_jobsubmit(jobid, 1); /* fresh xce_request_t (same seq/jobid) */
|
||||
|
||||
pthread_mutex_lock(&g_hc.persist.lock);
|
||||
lsn_before_second = g_hc.persist.current_lsn;
|
||||
pthread_mutex_unlock(&g_hc.persist.lock);
|
||||
|
||||
rc_second = handle_jobsubmit(&g_hc, &g_req_ep, &g_jobsub_req);
|
||||
|
||||
watermark_second = cbrp_poll_append(&g_hc.persist, lsn_before_second, 300);
|
||||
|
||||
cbrp_teardown();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-EEXIST, rc_second,
|
||||
"bug-1419: a resubmit for a jobid still in flight (durably "
|
||||
"appended, commit outcome unresolved) must be rejected -EEXIST, "
|
||||
"identically to a resubmit for a jobid already visible in "
|
||||
"c->jobmgr");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, (int)watermark_second,
|
||||
"bug-1419: a rejected resubmit must NOT append a second "
|
||||
"WAL_JOB_SUBMIT entry for the same jobid");
|
||||
|
||||
printf("test_jobsubmit_resubmit_before_resolution_rejected_no_dup_wal_entry: 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
|
||||
|
|
@ -3188,6 +3269,7 @@ void test_commit_barrier_reply(void)
|
|||
* FAILED failure-path coverage beyond the single-timeout tests above */
|
||||
RUN_TEST(test_jobsubmit_truncation_forces_failed_no_phantom_job);
|
||||
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_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