/* * test_jobsubmit_validate.c -- controller array admission * (job-array-one-based-index task 2.1, Requirements 7.1-7.6, 7.9). * * Two layers: * - jobsubmit_classify(first_jobid, size) over every 7.1-7.5 class, the * 7.9 precedence (an index-0 array wins over every other break), and * the no-wrap 7.2 bound (sizes of 2^32+2 and UINT64_MAX with index 2, * neither truncated nor wrapped). * - handle_jobsubmit end to end for rejected submissions: the reply * status, the 7.1 controller ERROR line, and that no job is stored * (7.6). Classification runs before the field checks, the hook and any * registry/table/persist access, so a rejected submission needs only a * reply endpoint, a job table to prove nothing was stored, and an * initialised hook system (never reached once classification exists). * * Reply observation: as in test_commit_barrier_reply.c, the requester * endpoint is a zeroed endpoint_t with no iomgr, so xce_reply() leaves the * encoded packet on ep->outq; it is drained with endpoint_deq() and parsed * with the raw capnp reader. * * Log observation: this test binary never calls tlog_init(), so ERROR() * falls through tlog's early-print path straight to STDOUT_FILENO (the * established precedent of test_worker_join.c's capture_stdout_during()). * * Task 2.2 (Requirements 7.7, 7.10, 7.11): answering a registered * submission and removing its in-flight registry entry are one step under * the registry lock. The fixture's persist has no WAL (wal_fd -1 and a * wal_path that cannot be created), so an admitted submission's local WAL * append fails synchronously and its commit continuation runs the async * failure path (reply once, then release the async context) inside * handle_jobsubmit -- deterministic, no thread. jobsubmit_test_reply_hook * observes the state between the reply and the unlink; * jobsubmit_test_release_hook registers a retry between the reply and the * context release. * * Task 2.3 (Requirements 7.10, 7.11): the job-table checks run after * registration. An existing element of the same array block (X|3 blocks a * new X|5; element 0 and element 32767 included) or of the element range * is answered -EEXIST through the atomic reply-and-release step, observed * with the same reply hook; array_identity_exists() is also driven directly. */ /* rtti.h must come first -- it defines 'var' and 'typeinfo_t' used by kvmgr.h */ #include #include #include #include #include #include #include #include #include #include #include "controller.capnp.h" #include "xce/cluster_def.h" #include "xce/errnox.h" #include "xce/types.h" #include "cluster.h" #include "hook.h" #include "jobj.h" #include "kvmgr.h" #include "netio.h" #include "request.h" #include "utils.h" /* ── Job identities ──────────────────────────────────────────────────── */ /* An array block (sequence aligned to 32768) at a fixed cluster/apiserver/ * timestamp; jsv_elem(k) is element k of it. */ #define JSV_CLUSTER 3 #define JSV_APISERVER 2 #define JSV_TS 1700000000ULL #define JSV_BLOCK ((uint64_t)5 << N_ARRAY_IDX_BITS) static uint64_t jsv_id(uint32_t tag, uint64_t seq) { return MAKE_JOBID(JSV_CLUSTER, JSV_APISERVER, JSV_TS, tag, seq); } static uint64_t jsv_elem(uint64_t k) { return jsv_id(TAG_JOB_ARRAY, JSV_BLOCK | k); } /* ── jobsubmit_classify ──────────────────────────────────────────────── */ typedef struct { const char* what; uint64_t first; uint64_t size; jobsubmit_class_t expect; } jsv_case_t; static void test_jobsubmit_classify_table(void) { const jsv_case_t cases[] = { /* 7.1: tagged array starting at element 0 */ { "7.1 index-0 array of 3", jsv_elem(0), 3, JOBSUBMIT_INDEX0 }, { "7.1 index-0 array of 1", jsv_elem(0), 1, JOBSUBMIT_INDEX0 }, { "7.1 index-0 array of 32768", jsv_elem(0), 32768, JOBSUBMIT_INDEX0 }, /* 7.9: 7.1 wins over every 7.2-7.5 break */ { "7.9 index-0 + size 0 (7.4)", jsv_elem(0), 0, JOBSUBMIT_INDEX0 }, { "7.9 index-0 + last > 32767 (7.2)", jsv_elem(0), 40000, JOBSUBMIT_INDEX0 }, { "7.9 index-0 + UINT64_MAX size", jsv_elem(0), UINT64_MAX, JOBSUBMIT_INDEX0 }, /* 7.4: size 0, any tag */ { "7.4 ordinary size 0", jsv_id(0, 77), 0, JOBSUBMIT_INVALID }, { "7.4 array size 0", jsv_elem(1), 0, JOBSUBMIT_INVALID }, /* 7.5: tags 2 and 3 (index 0 under them is not an array, so not * 7.1) */ { "7.5 tag 2", jsv_id(2, JSV_BLOCK | 1), 1, JOBSUBMIT_INVALID }, { "7.5 tag 3", jsv_id(3, JSV_BLOCK | 1), 1, JOBSUBMIT_INVALID }, { "7.5 tag 2 index 0", jsv_id(2, JSV_BLOCK), 3, JOBSUBMIT_INVALID }, { "7.5 tag 3 index 0 size 0", jsv_id(3, JSV_BLOCK), 0, JOBSUBMIT_INVALID }, /* 7.3: unmarked multi-element */ { "7.3 ordinary size 2", jsv_id(0, 77), 2, JOBSUBMIT_INVALID }, { "7.3 ordinary size 3", jsv_id(0, 9140), 3, JOBSUBMIT_INVALID }, { "7.3 ordinary UINT64_MAX", jsv_id(0, 77), UINT64_MAX, JOBSUBMIT_INVALID }, /* 7.2: last element above 32767, computed without addition */ { "7.2 idx 1 size 32768", jsv_elem(1), 32768, JOBSUBMIT_INVALID }, { "7.2 idx 2 size 32767", jsv_elem(2), 32767, JOBSUBMIT_INVALID }, { "7.2 idx 32767 size 2", jsv_elem(32767), 2, JOBSUBMIT_INVALID }, { "7.2 idx 2 size 2^32+2", jsv_elem(2), (1ULL << 32) + 2, JOBSUBMIT_INVALID }, { "7.2 idx 1 size 2^32+1", jsv_elem(1), (1ULL << 32) + 1, JOBSUBMIT_INVALID }, { "7.2 idx 2 size UINT64_MAX", jsv_elem(2), UINT64_MAX, JOBSUBMIT_INVALID }, /* accepted */ { "ok ordinary single", jsv_id(0, 77), 1, JOBSUBMIT_OK }, { "ok ordinary single seq 0", jsv_id(0, 0), 1, JOBSUBMIT_OK }, { "ok one-element array", jsv_elem(1), 1, JOBSUBMIT_OK }, { "ok array 1..3", jsv_elem(1), 3, JOBSUBMIT_OK }, { "ok array 1..32767", jsv_elem(1), 32767, JOBSUBMIT_OK }, { "ok array 2..32767", jsv_elem(2), 32766, JOBSUBMIT_OK }, { "ok array 32767..32767", jsv_elem(32767), 1, JOBSUBMIT_OK }, }; size_t i; char msg[160]; for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { snprintf(msg, sizeof(msg), "case %zu: %s", i, cases[i].what); TEST_ASSERT_EQUAL_INT_MESSAGE( cases[i].expect, jobsubmit_classify(cases[i].first, cases[i].size), msg); } } /* ── handle_jobsubmit fixture ────────────────────────────────────────── */ static cluster_t g_jsv_c; static endpoint_t g_jsv_ep; static ep_private_t g_jsv_priv; static job_startinfo_t g_jsv_startinfo; static job_submit_data_t g_jsv_data; static xce_job_submit_t g_jsv_msg; static xce_request_t g_jsv_req; static xce_peerinfo_t g_jsv_from; typedef struct { int n_replies; int32_t last_status; int32_t last_data_which; uint64_t last_seq; } jsv_reply_obs_t; static void jsv_setup(void) { kvmgr_params_t int_params = { .key_type = KVMGR_KEY_INT, .nslots = 64, .delta = 16 }; memset(&g_jsv_c, 0, sizeof(g_jsv_c)); memset(&g_jsv_ep, 0, sizeof(g_jsv_ep)); memset(&g_jsv_priv, 0, sizeof(g_jsv_priv)); g_jsv_ep.fd = -1; g_jsv_ep.data = &g_jsv_priv; g_jsv_c.myhost = &g_jsv_c.myhost_info; g_jsv_c.myhost_info.name = (char*)"ctrl-test"; g_jsv_c.myhost_info.key = (char*)""; TEST_ASSERT_EQUAL_INT( 0, xce_hook_init(&g_jsv_c.hooksys, XCE_CONTROLLER_HOOK_MAX)); g_jsv_c.jobmgr = makeInstance(&xkvmgr_type, &int_params); TEST_ASSERT_NOT_NULL(g_jsv_c.jobmgr); /* No WAL: an admitted submission's local append fails with -EIO (see * the file header). */ g_jsv_c.persist.wal_fd = -1; snprintf(g_jsv_c.persist.wal_path, sizeof(g_jsv_c.persist.wal_path), "/nonexistent-xce-jsv/wal"); } static void jsv_drain(jsv_reply_obs_t* obs); static void jsv_teardown(void) { jsv_reply_obs_t drain = { 0 }; jsv_drain(&drain); /* free any leftover packets */ jobsubmit_test_reply_hook = NULL; jobsubmit_test_release_hook = NULL; inflight_submits_reset_for_test(); if (g_jsv_c.jobmgr) dereference(g_jsv_c.jobmgr); xce_hook_fini(&g_jsv_c.hooksys); } /* A submission with every field the existing field checks require, unless * empty_queue is set (then the queue is "" and the field check would reject * it with -EINVAL). */ static void jsv_arrange(uint64_t first, uint64_t size, bool empty_queue) { memset(&g_jsv_startinfo, 0, sizeof(g_jsv_startinfo)); g_jsv_startinfo.cmdline = (char*)"/bin/true"; memset(&g_jsv_data, 0, sizeof(g_jsv_data)); g_jsv_data.queue = (char*)(empty_queue ? "" : "normal"); g_jsv_data.user = (char*)"tester"; g_jsv_data.start_info = &g_jsv_startinfo; memset(&g_jsv_msg, 0, sizeof(g_jsv_msg)); g_jsv_msg.jobid = first; g_jsv_msg.array_size = size; g_jsv_msg.submit = &g_jsv_data; g_jsv_from.name = (char*)"requester"; g_jsv_from.key = (char*)""; memset(&g_jsv_req, 0, sizeof(g_jsv_req)); g_jsv_req.opcode = OPCode_jobsub; g_jsv_req.seq = 21; g_jsv_req.from = &g_jsv_from; g_jsv_req.body_which = Request_body_jobsubmsg; g_jsv_req.body.jobsubmsg = &g_jsv_msg; } static void jsv_drain(jsv_reply_obs_t* obs) { data_item_t* it; while ((it = endpoint_deq(&g_jsv_ep)) != NULL) { io_packet_t* pkt = ITPTR_TO_PACKET(it); struct capn c; struct Request rq; Request_ptr rp; capn_init_mem(&c, (const uint8_t*)pkt->data, pkt->sz, 0); rp.p = capn_getp(capn_root(&c), 0, 1); read_Request(&rq, rp); if (rq.body_which == Request_body_replymsg) { struct RequestReply rr; read_RequestReply(&rr, rq.body.replymsg); obs->n_replies += 1; obs->last_status = rr.status; obs->last_data_which = rr.data_which; obs->last_seq = rr.seq; } capn_free(&c); free_packet(pkt); free(pkt); } } static bool jsv_job_exists(uint64_t jobid) { job_t* j = NULL; int rc; lock(g_jsv_c.jobmgr, RTTI_LOCK_READ); rc = key_find(g_jsv_c.jobmgr, (void*)jobid, (void**)&j); unlock(g_jsv_c.jobmgr); return rc > 0 && j != NULL; } /* Run handle_jobsubmit with STDOUT_FILENO redirected to a temp file; returns * the handler's rc and the captured output (caller frees). */ static int jsv_submit_capture(char** out) { char tmp[] = "/tmp/xce_jsv_logcap_XXXXXX"; int fd = mkstemp(tmp); int saved, rc; off_t len; char* buf; TEST_ASSERT_TRUE_MESSAGE(fd >= 0, "fixture: mkstemp must succeed"); fflush(stdout); saved = dup(STDOUT_FILENO); TEST_ASSERT_TRUE_MESSAGE(saved >= 0, "fixture: dup(stdout) must succeed"); TEST_ASSERT_TRUE_MESSAGE(dup2(fd, STDOUT_FILENO) >= 0, "fixture: dup2 redirect must succeed"); rc = handle_jobsubmit(&g_jsv_c, &g_jsv_ep, &g_jsv_req); fflush(stdout); dup2(saved, STDOUT_FILENO); close(saved); len = lseek(fd, 0, SEEK_CUR); TEST_ASSERT_TRUE(len >= 0); buf = (char*)calloc(1, (size_t)len + 1); TEST_ASSERT_NOT_NULL(buf); if (len > 0) { TEST_ASSERT_TRUE(lseek(fd, 0, SEEK_SET) == 0); TEST_ASSERT_TRUE(read(fd, buf, (size_t)len) == len); } close(fd); unlink(tmp); *out = buf; return rc; } /* Assert one reply with `status`, the handler returned it, and none of the * first min(size, 8) element identities is in the job table (7.6). */ static void jsv_expect_rejected(int rc, int32_t status, uint64_t first, uint64_t size, const char* what) { jsv_reply_obs_t obs = { 0 }; char msg[200]; uint64_t i, n = size < 8 ? size : 8; jsv_drain(&obs); snprintf(msg, sizeof(msg), "%s: exactly one reply", what); TEST_ASSERT_EQUAL_INT_MESSAGE(1, obs.n_replies, msg); snprintf(msg, sizeof(msg), "%s: reply status", what); TEST_ASSERT_EQUAL_INT_MESSAGE(status, obs.last_status, msg); snprintf(msg, sizeof(msg), "%s: reply carries no data", what); TEST_ASSERT_EQUAL_INT_MESSAGE(RequestReply_data_nodata, obs.last_data_which, msg); snprintf(msg, sizeof(msg), "%s: reply echoes the request seq", what); TEST_ASSERT_EQUAL_UINT64_MESSAGE(21, obs.last_seq, msg); snprintf(msg, sizeof(msg), "%s: handler return value", what); TEST_ASSERT_EQUAL_INT_MESSAGE(status, rc, msg); if (n == 0) n = 1; for (i = 0; i < n; i++) { snprintf(msg, sizeof(msg), "%s: no job stored at element %llu", what, (unsigned long long)i); TEST_ASSERT_FALSE_MESSAGE(jsv_job_exists(first + i), msg); } } /* 7.1 + 7.9: an index-0 array with an empty queue is answered with the * distinct status (not the field check's -EINVAL) and the ERROR line. */ static void test_jobsubmit_index0_empty_queue_start0_and_log(void) { const uint64_t first = jsv_elem(0); char* out = NULL; char idstr[64]; int rc; jsv_setup(); jsv_arrange(first, 3, true); rc = jsv_submit_capture(&out); jsv_expect_rejected(rc, -E_XCE_ARRAY_START0, first, 3, "index-0 array, empty queue"); fmt_jobid(idstr, sizeof(idstr), first); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "ERROR"), "7.1: an ERROR line must be logged"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, idstr), "7.1: the log line must name the job"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "start at 1"), "7.1: the log line must say array element " "numbers start at 1"); TEST_ASSERT_NOT_NULL_MESSAGE(strstr(out, "must be upgraded"), "7.1: the log line must say the submitting " "client must be upgraded"); free(out); jsv_teardown(); } /* 7.1 with every field valid: the same status and log, nothing stored. */ static void test_jobsubmit_index0_valid_fields_start0(void) { const uint64_t first = jsv_elem(0); char* out = NULL; int rc; jsv_setup(); jsv_arrange(first, 3, false); rc = jsv_submit_capture(&out); jsv_expect_rejected(rc, -E_XCE_ARRAY_START0, first, 3, "index-0 array, valid fields"); TEST_ASSERT_NOT_NULL(strstr(out, "must be upgraded")); free(out); jsv_teardown(); } /* 7.9: index 0 with size 0 -- 7.1 alone answers. */ static void test_jobsubmit_index0_size0_start0(void) { const uint64_t first = jsv_elem(0); char* out = NULL; int rc; jsv_setup(); jsv_arrange(first, 0, false); rc = jsv_submit_capture(&out); jsv_expect_rejected(rc, -E_XCE_ARRAY_START0, first, 0, "index-0 array, size 0"); free(out); jsv_teardown(); } /* 7.2-7.5: answered -EINVAL before any field check, hook or registry; no * 7.1 log line; nothing stored; the controller keeps serving (the next case * runs against the same handler). */ static void test_jobsubmit_invalid_classes_einval(void) { const struct { const char* what; uint64_t first; uint64_t size; bool empty_queue; } cases[] = { { "7.4 size 0", jsv_id(0, 9300), 0, false }, { "7.4 size 0, empty queue", jsv_id(0, 9301), 0, true }, { "7.5 tag 2", jsv_id(2, JSV_BLOCK | 1), 1, false }, { "7.5 tag 3", jsv_id(3, JSV_BLOCK | 1), 1, false }, { "7.3 unmarked size 3", jsv_id(0, 9310), 3, false }, { "7.2 idx 1 size 32768", jsv_elem(1), 32768, false }, { "7.2 idx 2 size 2^32+2", jsv_elem(2), (1ULL << 32) + 2, false }, { "7.2 idx 2 size UINT64_MAX", jsv_elem(2), UINT64_MAX, false }, }; size_t i; jsv_setup(); for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { char* out = NULL; int rc; jsv_arrange(cases[i].first, cases[i].size, cases[i].empty_queue); rc = jsv_submit_capture(&out); jsv_expect_rejected(rc, -EINVAL, cases[i].first, cases[i].size, cases[i].what); TEST_ASSERT_NULL_MESSAGE(strstr(out, "must be upgraded"), "7.2-7.5 rejections must not log the 7.1 " "line"); free(out); } jsv_teardown(); } /* ── Task 2.2: reply-and-remove atomicity ────────────────────────────── */ /* The array identity of jsv_elem(k): tag cleared, low 15 bits cleared. */ #define JSV_ARRAY_ID jsv_id(0, JSV_BLOCK) typedef struct { int calls; int trylock_rc; bool reply_queued; int entry_present; uint64_t base; uint64_t size; int is_array; uint64_t array_id; } jsv_hook_obs_t; static jsv_hook_obs_t g_jsv_hook; /* Runs inside inflight_reply_and_remove, after xce_reply, before the unlink, * with the registry lock held: records (1) that the lock is held, (2) that * the reply is already on the endpoint's outq, (3) that the entry is still * registered (lock-free accessor, since the lock is held). */ static void jsv_reply_hook(void) { g_jsv_hook.calls += 1; g_jsv_hook.trylock_rc = jobsubmit_inflight_trylock_for_test(); g_jsv_hook.reply_queued = (g_jsv_ep.outq.head != NULL); g_jsv_hook.entry_present = jobsubmit_inflight_overlaps_locked_for_test( g_jsv_hook.base, g_jsv_hook.size, g_jsv_hook.is_array, g_jsv_hook.array_id); } static void jsv_expect_atomic_reply(uint64_t first, uint64_t size, int is_array, uint64_t array_id, const char* what) { jsv_reply_obs_t obs = { 0 }; char* out = NULL; char msg[200]; int rc; jsv_setup(); memset(&g_jsv_hook, 0, sizeof(g_jsv_hook)); g_jsv_hook.base = first; g_jsv_hook.size = size; g_jsv_hook.is_array = is_array; g_jsv_hook.array_id = array_id; jobsubmit_test_reply_hook = jsv_reply_hook; jsv_arrange(first, size, false); rc = jsv_submit_capture(&out); free(out); snprintf(msg, sizeof(msg), "%s: the reply goes through the hook once", what); TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_jsv_hook.calls, msg); snprintf(msg, sizeof(msg), "%s: registry lock held across the reply", what); TEST_ASSERT_EQUAL_INT_MESSAGE(EBUSY, g_jsv_hook.trylock_rc, msg); snprintf(msg, sizeof(msg), "%s: reply already queued inside the hook", what); TEST_ASSERT_TRUE_MESSAGE(g_jsv_hook.reply_queued, msg); snprintf(msg, sizeof(msg), "%s: entry still registered inside the hook", what); TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_jsv_hook.entry_present, msg); snprintf(msg, sizeof(msg), "%s: entry gone after the reply", what); TEST_ASSERT_EQUAL_INT_MESSAGE( 0, jobsubmit_inflight_overlaps_for_test(first, size, is_array, array_id), msg); (void)rc; /* the async failure path returns 0 from the handler */ jsv_drain(&obs); snprintf(msg, sizeof(msg), "%s: exactly one reply", what); TEST_ASSERT_EQUAL_INT_MESSAGE(1, obs.n_replies, msg); snprintf(msg, sizeof(msg), "%s: commit-failure status", what); TEST_ASSERT_EQUAL_INT_MESSAGE(-EIO, obs.last_status, msg); TEST_ASSERT_FALSE(jsv_job_exists(first)); jsv_teardown(); } /* Single-job path: commit failure answered and released atomically. */ static void test_jobsubmit_reply_release_atomic_single(void) { jsv_expect_atomic_reply(jsv_id(0, 9400), 1, 0, 0, "single job"); } /* Array path (handle_jobsubmit_array): the same, for an array of 3. */ static void test_jobsubmit_reply_release_atomic_array(void) { jsv_expect_atomic_reply(jsv_elem(1), 3, 1, JSV_ARRAY_ID, "array of 3"); } /* One-element array (the single-job WAL path with a tagged ID). */ static void test_jobsubmit_reply_release_atomic_one_element_array(void) { jsv_expect_atomic_reply(jsv_elem(1), 1, 1, JSV_ARRAY_ID, "one-element array"); } static uint64_t g_jsv_retry_base, g_jsv_retry_size; static int g_jsv_retry_added; /* Runs when the original's async context is released, i.e. after its reply: * a retry of the same (base, size) registers here, as it may once the * original is answered (7.7). */ static void jsv_release_hook_register_retry(void) { if (g_jsv_retry_added == 0) g_jsv_retry_added = jobsubmit_inflight_try_add_for_test( g_jsv_retry_base, g_jsv_retry_size) ? 1 : -1; } static void jsv_expect_retry_survives(uint64_t first, uint64_t size, int is_array, uint64_t array_id, const char* what) { char* out = NULL; char msg[200]; jsv_setup(); g_jsv_retry_base = first; g_jsv_retry_size = size; g_jsv_retry_added = 0; jobsubmit_test_release_hook = jsv_release_hook_register_retry; jsv_arrange(first, size, false); (void)jsv_submit_capture(&out); free(out); snprintf(msg, sizeof(msg), "%s: the retry registers after the original's reply", what); TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_jsv_retry_added, msg); snprintf(msg, sizeof(msg), "%s: the original's context release leaves the retry " "registered", what); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, jobsubmit_inflight_overlaps_for_test(first, size, is_array, array_id), msg); jsv_teardown(); } static void test_jobsubmit_retry_survives_release_single(void) { jsv_expect_retry_survives(jsv_id(0, 9410), 1, 0, 0, "single job"); } static void test_jobsubmit_retry_survives_release_array(void) { jsv_expect_retry_survives(jsv_elem(1), 3, 1, JSV_ARRAY_ID, "array of 3"); } /* Registry overlap rule: element ranges intersect, or both entries are * arrays with the same array identity. */ static void test_jobsubmit_inflight_overlap_rule(void) { const uint64_t other_block = (uint64_t)9 << N_ARRAY_IDX_BITS; const uint64_t other_array = jsv_id(0, other_block); jsv_setup(); /* X|1..X|2 registered as an array */ TEST_ASSERT_EQUAL_INT(1, jobsubmit_inflight_try_add_for_test(jsv_elem(1), 2)); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, jobsubmit_inflight_overlaps_for_test(jsv_elem(2), 1, 1, JSV_ARRAY_ID), "intersecting element range overlaps"); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, jobsubmit_inflight_overlaps_for_test(jsv_elem(5), 1, 1, JSV_ARRAY_ID), "same array identity, disjoint elements: overlaps (7.10, 7.11)"); TEST_ASSERT_EQUAL_INT_MESSAGE( 0, jobsubmit_inflight_overlaps_for_test( jsv_id(TAG_JOB_ARRAY, other_block | 1), 3, 1, other_array), "a different array identity does not overlap"); TEST_ASSERT_EQUAL_INT_MESSAGE( 0, jobsubmit_inflight_overlaps_for_test(jsv_id(0, 9420), 1, 0, 0), "an ordinary job outside the range does not overlap"); /* the registry refuses a second same-identity array */ TEST_ASSERT_EQUAL_INT_MESSAGE( 0, jobsubmit_inflight_try_add_for_test(jsv_elem(5), 1), "a same-identity array cannot register while the first is"); /* an ordinary entry is compared by range only */ TEST_ASSERT_EQUAL_INT( 1, jobsubmit_inflight_try_add_for_test(jsv_id(0, 9421), 1)); TEST_ASSERT_EQUAL_INT( 0, jobsubmit_inflight_overlaps_for_test(jsv_id(0, 9422), 1, 0, 0)); jsv_teardown(); } /* 7.11: while X|1..X|2 is registered and unanswered, a submission X|5 with * the same array identity is answered duplicate-submission (-EEXIST) and * the first stays registered; once the first is answered (with a failure) * a new submission with that identity is admitted (7.7). */ static void test_jobsubmit_same_array_identity_while_registered(void) { jsv_reply_obs_t obs = { 0 }; char* out = NULL; int rc; jsv_setup(); memset(&g_jsv_hook, 0, sizeof(g_jsv_hook)); jobsubmit_test_reply_hook = jsv_reply_hook; TEST_ASSERT_EQUAL_INT(1, jobsubmit_inflight_try_add_for_test(jsv_elem(1), 2)); jsv_arrange(jsv_elem(5), 1, false); rc = jsv_submit_capture(&out); free(out); jsv_expect_rejected(rc, -EEXIST, jsv_elem(5), 1, "same array identity while registered"); TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_jsv_hook.calls, "a submission refused at registration " "has no entry to release"); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, jobsubmit_inflight_overlaps_for_test(jsv_elem(1), 2, 1, JSV_ARRAY_ID), "the first submission stays registered"); jsv_teardown(); /* answered: A fails its commit, then B with the same identity is * admitted (it reaches the WAL and fails there too, not -EEXIST) */ jsv_setup(); jsv_arrange(jsv_elem(1), 2, false); (void)jsv_submit_capture(&out); free(out); jsv_drain(&obs); TEST_ASSERT_EQUAL_INT(-EIO, obs.last_status); memset(&obs, 0, sizeof(obs)); jsv_arrange(jsv_elem(5), 1, false); (void)jsv_submit_capture(&out); free(out); jsv_drain(&obs); TEST_ASSERT_EQUAL_INT(1, obs.n_replies); TEST_ASSERT_EQUAL_INT_MESSAGE(-EIO, obs.last_status, "after the first is answered, a same-" "identity submission is admitted"); jsv_teardown(); } /* ── Task 2.3: job-table collisions after registration ───────────────── */ static job_submit_data_t g_jsv_seed_submit; /* Store an existing job `jobid` in the fixture's job table. */ static void jsv_seed_job(uint64_t jobid) { xce_job_t jdef; memset(&g_jsv_seed_submit, 0, sizeof(g_jsv_seed_submit)); g_jsv_seed_submit.queue = (char*)"normal"; g_jsv_seed_submit.user = (char*)"tester"; memset(&jdef, 0, sizeof(jdef)); jdef.jobid = jobid; jdef.state = JOB_STAT_PEND; jdef.submit = &g_jsv_seed_submit; TEST_ASSERT_NOT_NULL_MESSAGE(makeJob(g_jsv_c.jobmgr, &jdef), "fixture: seeding an existing job"); TEST_ASSERT_TRUE(jsv_job_exists(jobid)); } /* Submit (first, size) against a table holding `existing` and expect the * duplicate-submission reply: answered through inflight_reply_and_remove * while the submission is registered (so the table check ran after * registration), its entry released afterwards, and nothing new stored. */ static void jsv_expect_table_collision(uint64_t existing, uint64_t first, uint64_t size, const char* what) { char* out = NULL; char msg[200]; int is_array = is_array_tag(first) ? 1 : 0; uint64_t array_id = is_array ? array_jobid(first) : 0; uint64_t i; int rc; jsv_setup(); jsv_seed_job(existing); memset(&g_jsv_hook, 0, sizeof(g_jsv_hook)); g_jsv_hook.base = first; g_jsv_hook.size = size; g_jsv_hook.is_array = is_array; g_jsv_hook.array_id = array_id; jobsubmit_test_reply_hook = jsv_reply_hook; jsv_arrange(first, size, false); rc = jsv_submit_capture(&out); free(out); { jsv_reply_obs_t obs = { 0 }; jsv_drain(&obs); snprintf(msg, sizeof(msg), "%s: exactly one reply", what); TEST_ASSERT_EQUAL_INT_MESSAGE(1, obs.n_replies, msg); snprintf(msg, sizeof(msg), "%s: duplicate-submission reply", what); TEST_ASSERT_EQUAL_INT_MESSAGE(-EEXIST, obs.last_status, msg); snprintf(msg, sizeof(msg), "%s: reply echoes the request seq", what); TEST_ASSERT_EQUAL_UINT64_MESSAGE(21, obs.last_seq, msg); snprintf(msg, sizeof(msg), "%s: handler return value", what); TEST_ASSERT_EQUAL_INT_MESSAGE(-EEXIST, rc, msg); } snprintf(msg, sizeof(msg), "%s: the table hit is answered through the atomic " "reply-and-release step (registered first)", what); TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_jsv_hook.calls, msg); snprintf(msg, sizeof(msg), "%s: registry lock held across the reply", what); TEST_ASSERT_EQUAL_INT_MESSAGE(EBUSY, g_jsv_hook.trylock_rc, msg); snprintf(msg, sizeof(msg), "%s: entry registered while answered", what); TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_jsv_hook.entry_present, msg); snprintf(msg, sizeof(msg), "%s: entry released after the reply", what); TEST_ASSERT_EQUAL_INT_MESSAGE( 0, jobsubmit_inflight_overlaps_for_test(first, size, is_array, array_id), msg); snprintf(msg, sizeof(msg), "%s: the existing job is unchanged", what); TEST_ASSERT_TRUE_MESSAGE(jsv_job_exists(existing), msg); for (i = 0; i < size && i < 8; i++) { if (first + i == existing) continue; snprintf(msg, sizeof(msg), "%s: no job stored at offset %llu", what, (unsigned long long)i); TEST_ASSERT_FALSE_MESSAGE(jsv_job_exists(first + i), msg); } jsv_teardown(); } /* 7.10: an existing element X|3 blocks a new X|5 (same array identity, * disjoint elements) -- one-element and multi-element submissions. */ static void test_jobsubmit_existing_element_blocks_same_identity(void) { jsv_expect_table_collision(jsv_elem(3), jsv_elem(5), 1, "existing X|3, new X|5 (1 element)"); jsv_expect_table_collision(jsv_elem(3), jsv_elem(5), 4, "existing X|3, new X|5..X|8"); /* the identity scan covers the whole block, both ends */ jsv_expect_table_collision(jsv_elem(MAX_ARRAY_IDX), jsv_elem(1), 2, "existing X|32767, new X|1..X|2"); /* a pre-upgrade element 0 holds the identity too */ jsv_expect_table_collision(jsv_elem(0), jsv_elem(1), 3, "existing pre-upgrade X|0, new X|1..X|3"); } /* 7.10: the element-range check -- an existing element inside the range. */ static void test_jobsubmit_existing_element_in_range_blocks(void) { jsv_expect_table_collision(jsv_elem(2), jsv_elem(1), 3, "existing X|2, new X|1..X|3"); jsv_expect_table_collision(jsv_elem(1), jsv_elem(1), 1, "existing X|1, new X|1"); } /* Unmarked submissions keep the range check, now also after registration. */ static void test_jobsubmit_existing_ordinary_job_blocks(void) { jsv_expect_table_collision(jsv_id(0, 9500), jsv_id(0, 9500), 1, "existing ordinary job, same ID"); } /* No false collisions: another block's element, or an ordinary job whose * sequence lies in X's block, leaves X's identity free -- the submission is * admitted (it reaches the WAL and fails there with -EIO, not -EEXIST). */ static void test_jobsubmit_no_collision_admitted(void) { const uint64_t other_elem = jsv_id(TAG_JOB_ARRAY, ((uint64_t)9 << N_ARRAY_IDX_BITS) | 3); const uint64_t ordinary_in_block = jsv_id(0, JSV_BLOCK | 3); const struct { const char* what; uint64_t existing; uint64_t first; uint64_t size; } cases[] = { { "other block's element", other_elem, jsv_elem(5), 2 }, { "ordinary job in X's block", ordinary_in_block, jsv_elem(5), 1 }, { "array element, new ordinary job", jsv_elem(3), jsv_id(0, 9510), 1 }, }; size_t i; for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { jsv_reply_obs_t obs = { 0 }; char* out = NULL; jsv_setup(); jsv_seed_job(cases[i].existing); jsv_arrange(cases[i].first, cases[i].size, false); (void)jsv_submit_capture(&out); free(out); jsv_drain(&obs); TEST_ASSERT_EQUAL_INT_MESSAGE(1, obs.n_replies, cases[i].what); TEST_ASSERT_EQUAL_INT_MESSAGE(-EIO, obs.last_status, cases[i].what); jsv_teardown(); } } /* array_identity_exists: one scan over the block's 32768 element keys. */ static void test_array_identity_exists_scan(void) { const uint64_t other_block = (uint64_t)9 << N_ARRAY_IDX_BITS; jsv_setup(); TEST_ASSERT_EQUAL_INT_MESSAGE(0, array_identity_exists(&g_jsv_c, JSV_ARRAY_ID), "empty table: identity free"); jsv_seed_job(jsv_id(0, JSV_BLOCK | 3)); jsv_seed_job(jsv_id(TAG_JOB_ARRAY, other_block | 1)); TEST_ASSERT_EQUAL_INT_MESSAGE(0, array_identity_exists(&g_jsv_c, JSV_ARRAY_ID), "an ordinary job or another block's " "element does not hold X's identity"); jsv_seed_job(jsv_elem(MAX_ARRAY_IDX)); TEST_ASSERT_EQUAL_INT_MESSAGE(1, array_identity_exists(&g_jsv_c, JSV_ARRAY_ID), "element 32767 holds X's identity"); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, array_identity_exists(&g_jsv_c, jsv_id(0, other_block)), "the other block's identity is held too"); jsv_teardown(); jsv_setup(); jsv_seed_job(jsv_elem(0)); TEST_ASSERT_EQUAL_INT_MESSAGE(1, array_identity_exists(&g_jsv_c, JSV_ARRAY_ID), "a pre-upgrade element 0 holds the identity"); jsv_teardown(); } void test_jobsubmit_validate(void) { UnitySetTestFile(__FILE__); RUN_TEST(test_jobsubmit_classify_table); RUN_TEST(test_jobsubmit_index0_empty_queue_start0_and_log); RUN_TEST(test_jobsubmit_index0_valid_fields_start0); RUN_TEST(test_jobsubmit_index0_size0_start0); RUN_TEST(test_jobsubmit_invalid_classes_einval); RUN_TEST(test_jobsubmit_reply_release_atomic_single); RUN_TEST(test_jobsubmit_reply_release_atomic_array); RUN_TEST(test_jobsubmit_reply_release_atomic_one_element_array); RUN_TEST(test_jobsubmit_retry_survives_release_single); RUN_TEST(test_jobsubmit_retry_survives_release_array); RUN_TEST(test_jobsubmit_inflight_overlap_rule); RUN_TEST(test_jobsubmit_same_array_identity_while_registered); RUN_TEST(test_jobsubmit_existing_element_blocks_same_identity); RUN_TEST(test_jobsubmit_existing_element_in_range_blocks); RUN_TEST(test_jobsubmit_existing_ordinary_job_blocks); RUN_TEST(test_jobsubmit_no_collision_admitted); RUN_TEST(test_array_identity_exists_scan); }