From 7b3dfed56ed0971244d83ba787fa95eed9ae271f Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Sun, 23 Aug 2026 18:18:34 +0000 Subject: [PATCH] feat(scheduler-dispatch-commit): handle_jobdispatch no longer duplicates dispatch-outcome reporting (task 4.1) dispatch_a_job_() (tasks 3.1-3.3) now owns notification entirely for every outcome -- notify_dispatch() on a retryable failure, mark_job_fail()'s own hook chain on a terminal failure, nothing on success. handle_jobdispatch()'s loop still unconditionally called notify_dispatch() itself for every job regardless, producing two conflicting outcome reports for any failed dispatch. Removed that caller-side call; the loop is now a thin dispatch-and-free loop with no notification logic of its own. New test_jobdispatch_no_double_notify.c drives a 3-job batch (success, retryable, terminal) through handle_jobdispatch() and confirms exactly one notification fires (from the retryable job only), not four. Independently reviewed and approved. Confirmed handle_jobdispatch()'s sole caller never depended on its notification behavior (always returns 0 unconditionally). Flagged that test_commit_barrier_reply.c's test_jobdispatch_receipt_ack_no_wal_append is now stale for a second, independent reason (notify count, not just WAL count) -- both left for task 5.4 per Requirement 5.3. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN --- .../specs/scheduler-dispatch-commit/tasks.md | 4 +- src/controller/job.c | 20 +- src/controller/meson.build | 1 + src/controller/testsuite/controller_test.c | 2 + .../test_jobdispatch_no_double_notify.c | 357 ++++++++++++++++++ 5 files changed, 374 insertions(+), 10 deletions(-) create mode 100644 src/controller/testsuite/test_jobdispatch_no_double_notify.c diff --git a/.kiro/specs/scheduler-dispatch-commit/tasks.md b/.kiro/specs/scheduler-dispatch-commit/tasks.md index 6ad627c5..788b73ca 100644 --- a/.kiro/specs/scheduler-dispatch-commit/tasks.md +++ b/.kiro/specs/scheduler-dispatch-commit/tasks.md @@ -64,7 +64,7 @@ - _Depends: 1.1, 1.3, 3.2_ - [ ] 4. Integration: caller no longer duplicates dispatch-outcome reporting -- [ ] 4.1 Remove the caller's own dispatch-outcome reporting now that resolution owns it +- [x] 4.1 Remove the caller's own dispatch-outcome reporting now that resolution owns it - The function that invokes a dispatch attempt for each job in a batch no longer separately reports the outcome itself — reporting is now entirely owned by the dispatch resolution added in Task 3, preventing a failed dispatch from ever producing two different, conflicting outcome reports for the same job - Observable completion: a dispatch failure that resolves as permanent produces exactly one outcome report, not two conflicting ones - _Requirements: 3.1, 4.1, 4.2_ @@ -112,3 +112,5 @@ - Task 3.3: capturing a site's natural error code (`rc`/`errno`) into a stable local must happen BEFORE any intervening call once more logic is inserted before the eventual `return` — `errno` is not guaranteed stable across `lock()`/`ERROR()`/`free_launchinfo()` etc. This bit the worker-null and worker-not-ready sites specifically (both originally read `errno` directly at the `return` statement with nothing in between; task 3.3 had to capture it immediately after `SET_ERROR()`/at branch entry, before inserting the new lock/classify/resolve sequence). - Task 3.3: `test_commit_barrier_reply.c` (a file otherwise owned by the `ha-commit-barrier-rollout` spec) is this binary's sole host of the `notify()` stub — any future task needing to observe `notify_dispatch()`'s calls in a controller unit test must add accessors there too (purely additive, read-only `_ext`-suffixed pass-throughs to its existing static capture globals — do not modify the stub body itself or its existing pinned tests). Confirmed as legitimate shared test infrastructure, not a boundary violation, by task 3.3's independent review. - Task 3.3: the bound constant is `XCE_DISPATCH_FAIL_RETRY_BOUND = 3` (design.md's stated default), defined once near job.c's top and used at all four retryable sites — no magic numbers. 3 consecutive failures still retry; the 4th resolves terminal. +- Task 4.1: **`test_commit_barrier_reply.c`'s `test_jobdispatch_receipt_ack_no_wal_append` is now stale for a SECOND, independent reason**, on top of task 3.1's already-documented WAL-append-count staleness: its 2-job batch (one real-worker success, one `nhosts==0` terminal failure — no retryable job) now produces ZERO `notify_dispatch()` calls total post-task-4.1 (the success path never called it even before task 4.1, per task 3.1's design; the terminal path resolves via `mark_job_fail()`'s `schedFailJob` hook chain, not `notify_dispatch()`/`jobDispatchFail`, per task 3.2's design) — breaking its pinned `g_notify_count == 2` / both-`jobDispatchFail`-event assertions (lines ~1991-2003). **Task 5.4 must update BOTH the WAL-count assertion (task 3.1's finding) and the notify-count/event assertions (this finding) in the same test** before claiming full-suite regression-clean; the test's own scenario (1 success + 1 terminal, zero retryable jobs) may need to change too since it can no longer meaningfully exercise `notify_dispatch()` at all with today's cast of outcomes — consider adding a retryable-classified job to the batch if the intent is still to prove `notify_dispatch()`'s wiring through `handle_jobdispatch()`. +- Task 4.1: confirmed `handle_jobdispatch()`'s only caller (`cluster.c:471`, inside `myreq_handler`) uses its return value only as the generic RPC-handler `rc`, and `handle_jobdispatch()` itself always returns 0 unconditionally regardless of any job's outcome — nothing depends on the removed per-job notification behavior at the caller level. diff --git a/src/controller/job.c b/src/controller/job.c index 828196a6..41b3988c 100644 --- a/src/controller/job.c +++ b/src/controller/job.c @@ -1626,19 +1626,21 @@ int handle_jobdispatch(cluster_t* c, endpoint_t* ep, xce_request_t* request) xce_reply(ep, c->myhost, &reply); for (i = 0; i < dispatchmsg->njobs; i++) { - int rc1; xce_job_alloc_t* alloc = NULL; MOVE_PTR(alloc, dispatchmsg->allocs[i]); - rc1 = dispatch_a_job_(c, ep, alloc); - if (rc1 < 0) { - /* notify dispatch fail */ - notify_dispatch(c, ep, alloc->jobid, rc1); - } else { - /* notify dispatch ok */ - notify_dispatch(c, ep, alloc->jobid, 0); - } + /* scheduler-dispatch-commit task 4.1 (Req 3.1, 4.1, 4.2): reporting + * the dispatch outcome is now entirely owned by dispatch_a_job_() + * itself (tasks 3.1-3.3) -- it already calls notify_dispatch() on + * the retryable-failure path and relies on mark_job_fail()'s own + * hook chain for the terminal-failure path, never on success. This + * caller used to unconditionally call notify_dispatch() again for + * every job regardless of that outcome, producing a conflicting + * double report (e.g. both jobDispatchFail and schedFailJob for the + * same terminal failure). The return value has nothing left for + * this caller to act on. */ + (void)dispatch_a_job_(c, ep, alloc); free_JobAlloc_ptr(&(alloc)); } diff --git a/src/controller/meson.build b/src/controller/meson.build index 0a45dd47..f1a76c0f 100644 --- a/src/controller/meson.build +++ b/src/controller/meson.build @@ -182,6 +182,7 @@ controller_unit_test_src = [ 'testsuite/test_dispatch_commit.c', 'testsuite/test_dispatch_fail_terminal.c', 'testsuite/test_dispatch_fail_retry.c', + 'testsuite/test_jobdispatch_no_double_notify.c', 'testsuite/test_upgrade_snapshot_restore.c', 'testsuite/test_upgrade_snapshot_apply.c', 'testsuite/test_bug857_regression.c', diff --git a/src/controller/testsuite/controller_test.c b/src/controller/testsuite/controller_test.c index cc19fbec..3b5148e9 100644 --- a/src/controller/testsuite/controller_test.c +++ b/src/controller/testsuite/controller_test.c @@ -100,6 +100,7 @@ extern void test_job_dispatch(void); extern void test_dispatch_commit(void); extern void test_dispatch_fail_terminal(void); extern void test_dispatch_fail_retry(void); +extern void test_jobdispatch_no_double_notify(void); void setUp(void) { return; } @@ -269,6 +270,7 @@ int main(int argc, char* argv[]) TEST(test_dispatch_commit); TEST(test_dispatch_fail_terminal); TEST(test_dispatch_fail_retry); + TEST(test_jobdispatch_no_double_notify); return UNITY_END(); } diff --git a/src/controller/testsuite/test_jobdispatch_no_double_notify.c b/src/controller/testsuite/test_jobdispatch_no_double_notify.c new file mode 100644 index 00000000..bab8a091 --- /dev/null +++ b/src/controller/testsuite/test_jobdispatch_no_double_notify.c @@ -0,0 +1,357 @@ +/* test_jobdispatch_no_double_notify.c -- scheduler-dispatch-commit task 4.1 + * + * Proves handle_jobdispatch() (job.c) no longer duplicates the dispatch- + * outcome reporting that dispatch_a_job_() (tasks 3.1-3.3) now owns + * entirely. Before this task, handle_jobdispatch()'s loop called + * notify_dispatch() itself for EVERY job in the batch regardless of + * dispatch_a_job_()'s own outcome/classification -- on top of whatever + * dispatch_a_job_() had already decided to do (nothing on success or on a + * terminal failure, exactly one notify_dispatch() call on a retryable + * failure). That produced a conflicting double report for a terminal + * failure specifically (a caller-added jobDispatchFail alongside + * mark_job_fail()'s own schedFailJob -- design.md's round-2 finding) and a + * redundant extra notify_dispatch() call for a retryable failure. + * + * This test drives handle_jobdispatch() directly (not dispatch_a_job_test()) + * with a 3-job batch covering all three of dispatch_a_job_()'s own + * notify_dispatch() behaviors: + * - success (a live, ready worker) -> dispatch_a_job_() itself + * calls notify_dispatch() 0 times + * - retryable failure (unregistered worker) -> dispatch_a_job_() itself + * calls notify_dispatch() exactly 1 time (below the retry bound) + * - terminal failure (nhosts == 0) -> dispatch_a_job_() itself + * calls notify_dispatch() 0 times (mark_job_fail()'s hook chain is the + * sole notification for this case) + * + * dispatch_a_job_() alone (already proven correct by tasks 3.1-3.3's own + * test_dispatch_commit.c / test_dispatch_fail_terminal.c / + * test_dispatch_fail_retry.c) therefore produces exactly ONE + * notify_dispatch() call for this batch. The fix under test is that + * handle_jobdispatch() must not add anything on top of that -- the total + * observed via notify() (test_commit_barrier_reply.c's capture stub, the + * sole notify() definition linked into this binary) must be exactly 1, not + * 4 (1 extra per job x 3, on top of the 1 dispatch_a_job_() already + * produces) as it was before this task. + * + * Fixture mirrors test_dispatch_commit.c's dc_setup/dc_teardown (cluster_t + * with a real disk-backed persist_t, cluster == NULL routing so + * persist_submit_job_fact() takes the direct persist_wal_async() path, no + * HA/commit-engine machinery needed) plus test_commit_barrier_reply.c's + * g_req_ep / c->myhost requester-endpoint pattern (zeroed, no iomgr -- + * xce_reply()'s enqueue always succeeds against it, proven by that file's + * own header) so handle_jobdispatch()'s receipt-ACK xce_reply() call has + * somewhere to enqueue to. + * + * notify_dispatch() observability: see test_dispatch_fail_retry.c's header + * for the full rationale -- this binary's sole notify() definition lives in + * test_commit_barrier_reply.c, exposed read-only via its + * cbrp_notify_capture_*_ext() accessors (added by task 3.3). + */ + +#include + +#include +#include +#include +#include +#include + +#include + +#include "cluster.h" +#include "controller.capnp.h" +#include "dataq.h" +#include "hook.h" +#include "jobj.h" +#include "kvmgr.h" +#include "netio.h" +#include "persist.h" +#include "qobj.h" +#include "typemap.h" +#include "wobj.h" +#include "xce/cluster_def.h" +#include "xce/types.h" +#include "xcealloc.h" +#include "xstring.h" + +/* test_commit_barrier_reply.c's minimal notify()-capture accessors (task + * 3.3) -- see this file's header. This binary's sole notify() definition + * lives there (dataex.c's real one is excluded from the unit-test link). */ +extern int cbrp_notify_capture_count_ext(void); +extern int cbrp_notify_capture_event_ext(int idx); +extern uint64_t cbrp_notify_capture_jobid_ext(int idx); +extern int cbrp_notify_capture_code_ext(int idx); +extern void cbrp_notify_capture_reset_ext(void); + +/* jobj.c's job type table and the process-global xstring table are shared + * across every test file in the controller_unit_test binary -- guard + * idempotently, mirroring every sibling fixture in this suite. */ +static int g_jnd_jobobj_ready; + +static cluster_t g_jnd; +static char g_jnd_tmpdir[256]; +static endpoint_t g_jnd_req_ep; /* requester endpoint (receipt ACK) */ +static endpoint_t g_jnd_worker_ep; /* the one live worker's endpoint */ +static xce_peerinfo_t g_jnd_req_from; + +static void jnd_setup(void) +{ + kvmgr_params_t int_params = { .key_type = KVMGR_KEY_INT, + .nslots = 32, .delta = 16 }; + kvmgr_params_t str_params = { .key_type = KVMGR_KEY_STRING, + .nslots = 32, .delta = 16 }; + persist_config_t cfg; + + if (makeString((char*)"jnd-probe") == NULL) + xstring_table_init(64); + + if (!g_jnd_jobobj_ready) { + TEST_ASSERT_EQUAL_INT(0, job_obj_init(32, 32)); + g_jnd_jobobj_ready = 1; + } + + memset(&g_jnd, 0, sizeof(g_jnd)); + TEST_ASSERT_EQUAL_INT(0, + xce_hook_init(&g_jnd.hooksys, XCE_CONTROLLER_HOOK_MAX)); + + g_jnd.jobmgr = makeInstance(&xkvmgr_type, &int_params); + g_jnd.workermgr = makeInstance(&xkvmgr_type, &str_params); + g_jnd.qmgr = makeInstance(&xkvmgr_type, &str_params); + TEST_ASSERT_NOT_NULL(g_jnd.jobmgr); + TEST_ASSERT_NOT_NULL(g_jnd.workermgr); + TEST_ASSERT_NOT_NULL(g_jnd.qmgr); + + /* Real disk-backed persist, cluster == NULL: wal_hook_append_ + * classified() takes the direct persist_wal_async() path -- no raft/ + * commit-engine machinery needed for the successful job's plain + * WAL_JOB_DISPATCH fact append (mirrors test_dispatch_commit.c). */ + cfg = persist_config_defaults(); + cfg.wal_sync_ms = 5000; + strcpy(g_jnd_tmpdir, "/tmp/xce_jnd41_XXXXXX"); + TEST_ASSERT_NOT_NULL(mkdtemp(g_jnd_tmpdir)); + persist_set_workdir(&g_jnd.persist, g_jnd_tmpdir, "ctrl-test"); + TEST_ASSERT_EQUAL_INT(0, persist_init(&g_jnd.persist, &cfg, NULL)); + TEST_ASSERT_EQUAL_INT(0, persist_start(&g_jnd.persist)); + + g_jnd.myhost = &g_jnd.myhost_info; + g_jnd.myhost_info.name = (char*)"ctrl-test"; + g_jnd.myhost_info.key = (char*)""; + + /* Zeroed, no iomgr -- xce_reply()'s/xce_startjob_req()'s enqueue always + * succeeds against an endpoint like this (test_dispatch_commit.c's and + * test_commit_barrier_reply.c's own established, proven pattern). */ + memset(&g_jnd_req_ep, 0, sizeof(g_jnd_req_ep)); + g_jnd_req_ep.fd = -1; + + memset(&g_jnd_worker_ep, 0, sizeof(g_jnd_worker_ep)); + g_jnd_worker_ep.fd = -1; + + g_jnd_req_from.name = (char*)"requester"; + g_jnd_req_from.key = (char*)""; +} + +/* Frees every packet left queued on an endpoint's outq -- mirrors + * test_dispatch_commit.c's dc_drain_ep. */ +static void jnd_drain_ep(endpoint_t* ep) +{ + data_item_t* it; + + while ((it = endpoint_deq(ep)) != NULL) { + io_packet_t* pkt = ITPTR_TO_PACKET(it); + free_packet(pkt); + free(pkt); + } +} + +static void jnd_teardown(void) +{ + char cmd[512]; + + jnd_drain_ep(&g_jnd_req_ep); + jnd_drain_ep(&g_jnd_worker_ep); + + persist_stop(&g_jnd.persist); + persist_fini(&g_jnd.persist); + + if (g_jnd.workermgr) dereference(g_jnd.workermgr); + if (g_jnd.qmgr) dereference(g_jnd.qmgr); + if (g_jnd.jobmgr) dereference(g_jnd.jobmgr); + xce_hook_fini(&g_jnd.hooksys); + + snprintf(cmd, sizeof(cmd), "rm -rf %s", g_jnd_tmpdir); + if (system(cmd) != 0) + fprintf(stderr, "cleanup warning: rm -rf %s failed\n", g_jnd_tmpdir); +} + +/* Seeds a queue and a PEND job with a real submit/start_info against + * g_jnd.jobmgr/qmgr (dispatch_a_job_() reads submit->queue and + * submit->start_info). Returns the seeded job_t* (borrowed -- g_jnd.jobmgr + * owns the creation reference). Mirrors test_dispatch_fail_retry.c's + * dfr_seed_job. */ +static job_t* jnd_seed_job(uint64_t jobid, const char* qname) +{ + xce_queue_t qdef; + job_startinfo_t startinfo; + job_submit_data_t submit; + xce_job_t jdef; + job_t* j; + + memset(&qdef, 0, sizeof(qdef)); + qdef.queue = (char*)qname; + TEST_ASSERT_NOT_NULL(makeQueue(g_jnd.qmgr, &qdef)); + + memset(&startinfo, 0, sizeof(startinfo)); + startinfo.cmdline = (char*)"/bin/true"; + memset(&submit, 0, sizeof(submit)); + submit.queue = (char*)qname; + submit.user = (char*)"tester"; + submit.start_info = &startinfo; + + memset(&jdef, 0, sizeof(jdef)); + jdef.jobid = jobid; + jdef.state = JOB_STAT_PEND; + jdef.submit = &submit; + j = makeJob(g_jnd.jobmgr, &jdef); + TEST_ASSERT_NOT_NULL(j); + + return j; +} + +/* Registers a ready worker (host.status defaults to 0 via calloc) in + * g_jnd.workermgr with ep pointed at the given endpoint. Mirrors + * test_dispatch_commit.c's dc_seed_ready's worker half. */ +static void jnd_seed_ready_worker(const char* wname, endpoint_t* ep) +{ + xce_worker_t wdef; + hostattrs_t* attrs; + worker_t* w = NULL; + + attrs = calloc(1, sizeof(*attrs)); + TEST_ASSERT_NOT_NULL(attrs); + attrs->hosttype = makeString("LINUX"); + attrs->hostmodel = makeString("x86"); + attrs->cpufactor = 1.0f; + attrs->maxcpus = 4; + memset(&wdef, 0, sizeof(wdef)); + wdef.name = (char*)wname; + wdef.attrs = attrs; + TEST_ASSERT_NOT_NULL(makeWorker(g_jnd.workermgr, &wdef)); + + lock(g_jnd.workermgr, RTTI_LOCK_READ); + key_find(g_jnd.workermgr, (void*)wname, (void**)&w); + unlock(g_jnd.workermgr); + TEST_ASSERT_NOT_NULL(w); + w->ep = ep; +} + +#define JND_JOBID_SUCCESS 92101ULL +#define JND_JOBID_RETRYABLE 92102ULL +#define JND_JOBID_TERMINAL 92103ULL + +/* THE core proof (Req 3.1, 4.1, 4.2): a batch mixing all three of + * dispatch_a_job_()'s own notify_dispatch() behaviors must produce EXACTLY + * what dispatch_a_job_() itself produces (1 -- the retryable job's own + * call), not 1 extra per job from handle_jobdispatch()'s own loop. */ +static void test_handle_jobdispatch_matches_dispatch_a_job_notify_count(void) +{ + xce_job_alloc_t* allocs[3]; + xce_job_dispatch_t dispatchmsg; + xce_request_t req; + job_t* jsucc; + job_t* jretry; + job_t* jterm; + int rc; + + jnd_setup(); + + jsucc = jnd_seed_job(JND_JOBID_SUCCESS, "jnd-q1"); + jretry = jnd_seed_job(JND_JOBID_RETRYABLE, "jnd-q2"); + jterm = jnd_seed_job(JND_JOBID_TERMINAL, "jnd-q3"); + + jnd_seed_ready_worker("jnd-wkr-ok", &g_jnd_worker_ep); + /* jretry deliberately targets an unregistered worker name -- no + * corresponding makeWorker() call, matching test_dispatch_fail_retry.c's + * own established WORKER_NULL/WORKER_LOOKUP fixture. */ + + allocs[0] = make_job_alloc(JND_JOBID_SUCCESS, 1, + make_host_allocs(1, + make_host_alloc((char*)"jnd-wkr-ok", 1, + make_allocs(1, + make_alloc((char*)"slots", NULL, 0, 1.0f, 0))))); + TEST_ASSERT_NOT_NULL(allocs[0]); + + allocs[1] = make_job_alloc(JND_JOBID_RETRYABLE, 1, + make_host_allocs(1, + make_host_alloc((char*)"jnd-no-such-worker", 1, + make_allocs(1, + make_alloc((char*)"slots", NULL, 0, 1.0f, 0))))); + TEST_ASSERT_NOT_NULL(allocs[1]); + + /* nhosts == 0: XCE_DISPATCH_FAIL_SITE_INVALID_ALLOC, terminal -- + * mirrors test_commit_barrier_reply.c's cbrp_arrange_jobdispatch() + * second allocation exactly. */ + allocs[2] = make_job_alloc(JND_JOBID_TERMINAL, 0, NULL); + TEST_ASSERT_NOT_NULL(allocs[2]); + + memset(&dispatchmsg, 0, sizeof(dispatchmsg)); + dispatchmsg.njobs = 3; + dispatchmsg.allocs = allocs; + + memset(&req, 0, sizeof(req)); + req.opcode = OPCode_jobdispatch; + req.seq = 42; + req.from = &g_jnd_req_from; + req.body_which = Request_body_dispatchmsg; + req.body.dispatchmsg = &dispatchmsg; + + cbrp_notify_capture_reset_ext(); + + /* Act: handle_jobdispatch() consumes/frees every alloc itself via + * MOVE_PTR + free_JobAlloc_ptr() in its own loop -- nothing left for + * this test to free afterward. */ + rc = handle_jobdispatch(&g_jnd, &g_jnd_req_ep, &req); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "handle_jobdispatch's own return " + "is unconditionally 0, unaffected by any per-job outcome"); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_RUN, jsucc->job.state, + "the successful dispatch must still commit RUN (unaffected by this " + "task -- only the caller's own notification logic changes)"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_PEND, jretry->job.state, + "a below-bound retryable failure must still leave the job PEND"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, jretry->dispatch_fail_count, + "the retryable failure's own counter increment is unaffected by " + "this task"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_EXIT, jterm->job.state, + "the terminal (nhosts == 0) failure must still resolve to EXIT via " + "mark_job_fail(), unaffected by this task"); + + /* THE assertion this task exists for: dispatch_a_job_() alone (tasks + * 3.1-3.3, already proven correct) produces exactly ONE + * notify_dispatch() call for this exact batch -- the retryable job's + * own call. Before this task, handle_jobdispatch()'s own unconditional + * per-job notify_dispatch() calls would add one more per job (3), for a + * total of 4 -- a conflicting double (and, for the terminal job, + * mismatched) report. */ + TEST_ASSERT_EQUAL_INT_MESSAGE(1, cbrp_notify_capture_count_ext(), + "handle_jobdispatch() must not add any notify_dispatch() call on " + "top of what dispatch_a_job_() itself already produced for this " + "batch (Req 3.1, 4.1, 4.2) -- exactly the retryable job's own " + "call, none for the success or terminal jobs"); + TEST_ASSERT_EQUAL_INT(NotifyData_data_jobDispatchFail, + cbrp_notify_capture_event_ext(0)); + TEST_ASSERT_EQUAL_UINT64(JND_JOBID_RETRYABLE, + cbrp_notify_capture_jobid_ext(0)); + + jnd_teardown(); + printf("test_handle_jobdispatch_matches_dispatch_a_job_notify_count: " + "PASS\n"); +} + +void test_jobdispatch_no_double_notify(void) +{ + UnitySetTestFile(__FILE__); + printf("\n>> handle_jobdispatch() no-double-notify tests (Req 3.1, 4.1, " + "4.2)\n\n"); + + RUN_TEST(test_handle_jobdispatch_matches_dispatch_a_job_notify_count); +}