feat(job-array-one-based-index): collision checks after registration (task 2.3)

handle_jobsubmit now registers the submission first and checks the job
table second; a hit is answered -EEXIST and released through the atomic
reply-and-release step. For array submissions, array_identity_exists()
does one read-locked scan of the 32768 tagged element keys of the
array's block (element 0 included for pre-upgrade arrays), covering
both the element range and the array identity; unmarked submissions
keep jobid_range_exists. An existing X|3 now blocks a new X|5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-01 10:14:47 +00:00
parent 81bfaf1f50
commit fe5ddc9854
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
5 changed files with 278 additions and 27 deletions

View file

@ -33,6 +33,12 @@
* 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
@ -699,6 +705,196 @@ static void test_jobsubmit_same_array_identity_while_registered(void)
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__);
@ -714,4 +910,9 @@ void test_jobsubmit_validate(void)
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);
}