fix(job-array-single-wal-record): free jobArray dataset on scheduler receive path

sched_handle_dataset()'s Data_data_jobArray case never freed
dataset->rows (the decoded xce_job_submit_t*) after sched_on_job_array()
consumed it -- free_Dataset() is an empty no-op for every table type, so
nothing else owned this call. One bounded leak per delivered array push.

Free it via the existing free_JobSubmit_ptr() teardown on both the
success and error path, mirroring the sibling list-shaped cases'
ownership-transfer convention.

Rewrote test_job_array_expand.c's init_job_array() fixture to
heap-allocate + strdup its fields (matching the real wire-decode shape)
since freeing the old stack/string-literal fixture crashed. Redelivery
test now builds a fresh descriptor per delivery, matching how a real
redelivery is always a freshly-decoded wire message.

Closes task 7.3 (job-array-single-wal-record), the last of the three
post-GO residual gaps reopened 2026-07-16.
This commit is contained in:
Rongsong Shen 2026-07-17 07:13:12 +00:00
parent ea8d9d3c4f
commit 53bd686897
3 changed files with 55 additions and 33 deletions

View file

@ -86,23 +86,32 @@ static void seed_existing_job(xce_scheduler_t* sched, uint64_t jobid)
sched_add_job(sched, job);
}
/* Fills a caller-owned (jobArray, submit) pair with a small, valid, sharable
* job spec -- the same (jobid, arraySize, submit) shape task 5.1's
* xcesched_push_jobarray() sends. */
static void init_job_array(xce_job_submit_t* ja, job_submit_data_t* submit,
uint64_t base_jobid, uint64_t array_size)
/* Builds a heap-allocated, heap-owned-strings (jobArray, submit) pair -- the
* same shape decode_JobSubmit_ptr() produces on the real wire-decode path
* (controller.capnp.c), not a stack/string-literal stand-in. Task 7.3 made
* sched_handle_dataset()'s Data_data_jobArray case free_JobSubmit_ptr() its
* dataset->rows unconditionally, so any fixture routed through that handler
* must be genuinely heap-owned or the free segfaults on stack memory /
* string literals. Caller owns the returned pointer until it hands it to
* sched_handle_dataset() (which frees it) or frees it directly itself. */
static xce_job_submit_t* init_job_array(uint64_t base_jobid,
uint64_t array_size)
{
memset(submit, 0, sizeof(*submit));
submit->queue = "normal";
submit->user = "tester";
job_submit_data_t* submit
= (job_submit_data_t*)calloc(1, sizeof(*submit));
xce_job_submit_t* ja = (xce_job_submit_t*)calloc(1, sizeof(*ja));
submit->queue = strdup("normal");
submit->user = strdup("tester");
submit->mintasks = 1;
submit->maxtasks = 1;
submit->resreq = "type == local";
submit->resreq = strdup("type == local");
memset(ja, 0, sizeof(*ja));
ja->jobid = base_jobid;
ja->array_size = array_size;
ja->submit = submit;
return ja;
}
/* Pop one pending update request, asserting it is an UPDATE_JOB op; hands
@ -135,20 +144,21 @@ static int pop_job_update(xce_scheduler_t* sched, xce_job_t*** jobs_out)
static void test_job_array_push_expands_into_n_tasks(void)
{
xce_scheduler_t sched;
job_submit_data_t submit;
xce_job_submit_t ja;
xce_job_submit_t* ja;
dataset_t dataset;
xce_job_t** jobs = NULL;
int njobs;
int i;
fixture_init(&sched);
init_job_array(&ja, &submit, 5000, 3);
ja = init_job_array(5000, 3);
memset(&dataset, 0, sizeof(dataset));
dataset.table = Data_data_jobArray;
dataset.rows = (void*)&ja;
dataset.rows = (void*)ja;
/* sched_handle_dataset() owns and frees dataset.rows on return (task
* 7.3) -- do not touch ja/dataset.rows again after this call. */
TEST_ASSERT_TRUE(sched_handle_dataset(&sched, &dataset) >= 0);
njobs = pop_job_update(&sched, &jobs);
@ -177,21 +187,19 @@ static void test_job_array_push_expands_into_n_tasks(void)
static void test_job_array_redelivery_does_not_duplicate(void)
{
xce_scheduler_t sched;
job_submit_data_t submit;
xce_job_submit_t ja;
dataset_t dataset;
xce_job_t** jobs = NULL;
int njobs;
int i;
fixture_init(&sched);
init_job_array(&ja, &submit, 6000, 2);
memset(&dataset, 0, sizeof(dataset));
dataset.table = Data_data_jobArray;
dataset.rows = (void*)&ja;
dataset.rows = (void*)init_job_array(6000, 2);
/* first delivery: 2 new tasks enqueued */
/* first delivery: 2 new tasks enqueued. sched_handle_dataset() owns and
* frees dataset.rows on return (task 7.3). */
TEST_ASSERT_TRUE(sched_handle_dataset(&sched, &dataset) >= 0);
njobs = pop_job_update(&sched, &jobs);
TEST_ASSERT_EQUAL_INT(2, njobs);
@ -206,7 +214,12 @@ static void test_job_array_redelivery_does_not_duplicate(void)
}
free(jobs);
/* redelivery of the IDENTICAL push: every index already exists */
/* redelivery of the IDENTICAL push: a genuinely new decode of the
* retransmitted wire message (same jobid/array_size/submit content, but
* its own freshly-allocated descriptor -- exactly what a real reconnect
* redelivers; the first descriptor is already freed by the call above).
* Every index already exists. */
dataset.rows = (void*)init_job_array(6000, 2);
TEST_ASSERT_EQUAL_INT(0, sched_handle_dataset(&sched, &dataset));
TEST_ASSERT_EQUAL_INT(-1, pop_job_update(&sched, NULL)); /* nothing new */
}
@ -231,13 +244,17 @@ static void test_job_array_null_submit_returns_einval(void)
static void test_job_array_zero_size_returns_einval(void)
{
xce_scheduler_t sched;
job_submit_data_t submit;
xce_job_submit_t ja;
xce_job_submit_t* ja;
fixture_init(&sched);
init_job_array(&ja, &submit, 42, 0);
ja = init_job_array(42, 0);
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_on_job_array(&sched, &ja));
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_on_job_array(&sched, ja));
/* unlike sched_handle_dataset(), sched_on_job_array() never takes
* ownership of job_array -- this test built it directly, so it frees it
* directly. */
free_JobSubmit_ptr(&ja);
}
static void test_job_array_null_descriptor_returns_einval(void)
@ -335,13 +352,12 @@ static void test_wire_dispatch_chain_delivers_n_tasks(void)
/* Cleanup: extract_data() transferred ownership of the decoded
* xce_job_submit_t into dataset.rows (nulling the source pointer inside
* decoded.body.pushmsg); sched_handle_dataset()'s jobArray case
* deliberately does not null dataset.rows out (every element is its own
* clone -- see that function's own comment), so this test, as the
* descriptor's owner, frees it directly. decoded.body.pushmsg is now a
* decoded.body.pushmsg); sched_handle_dataset()'s jobArray case now
* frees dataset.rows itself and nulls it out (task 7.3), so there is
* nothing left for this test to free there. decoded.body.pushmsg is a
* hollow container, safe to free via the normal request teardown. */
TEST_ASSERT_NULL(dataset.rows);
xce_free_request(&decoded);
free_JobSubmit_ptr((xce_job_submit_t **)&dataset.rows);
}
int main(int argc, char* argv[])