fix(job-array-single-wal-record): wire dispatcher was silently dropping compact array pushes
extract_data() (src/lib/request.c), the shared reassembly step every inbound push passes through inside vtask_entry() before any receiver's handler runs, had no case for Data_data_jobArray -- it hit default: rc=-EINVAL. Because vtask_entry()'s callnext check treats any nonzero return (including a negative errno) as "reassembly complete, proceed," this never surfaced as an error: the real payload was freed and the receiver silently handed an empty, zeroed dataset_t, with no log anywhere in the chain. Tasks 5.1-5.3 were each individually correct and tested in isolation, but the feature was dead on the real wire end-to-end. Adds case Data_data_jobArray: passing the single, unchunked descriptor straight through (no pagination needed, unlike the existing LIST-shaped cases) and nulls the source pointer after transfer -- verified this is not cosmetic, since free_Request()'s pushmsg case would otherwise double-free it via free_Data()'s live Data_data_jobArray branch. Proven with a real xce_encode()/xce_decode() round-trip through the fixed extract_data() -> sched_handle_dataset() -> sched_on_job_array() (task 5.3, unmodified) chain, not just a unit-level check on extract_data() alone. Also confirms task 5.4's future API-server receive path shares this exact fix, since both go through the same xcecli_ctx_init() -> vtask_entry() -> extract_data() pipeline. One self-flagged, non-blocking residual gap (pre-existing since task 5.3, out of this task's request.c-only boundary): dataset->rows for the jobArray case is never deep-freed downstream, leaking one bounded xce_job_submit_t per successfully-delivered array push commit. Task 5.3b of job-array-single-wal-record (Requirements 6.1, 6.2, 6.4, 8.4). Inserted mid-run after independent review confirmed the gap; user confirmed sequencing (fix now, before task 5.4).
This commit is contained in:
parent
d16847185e
commit
1cbd7df1a8
4 changed files with 216 additions and 1 deletions
|
|
@ -1778,6 +1778,27 @@ int extract_data(dataset_t* dataset, xce_data_t* getdata)
|
|||
|
||||
memset(getdata->data.resvals->vals, 0x0, count * sizeof(xce_resval_t*));
|
||||
break;
|
||||
case Data_data_jobArray:
|
||||
/* job-array-single-wal-record task 5.3b: single, unchunked JobSubmit
|
||||
* descriptor (base jobid, array_size, one shared spec) -- unlike the
|
||||
* LIST-shaped cases above, no pagination/accumulation loop is needed:
|
||||
* xcesched_push_jobarray() (sched.c) and apiserver_push_jobarray()
|
||||
* (apiserver.c) always send exactly one complete descriptor per
|
||||
* commit. Transfer ownership of the decoded pointer straight into
|
||||
* dataset->rows (mirrors the LIST cases' ownership-transfer intent,
|
||||
* without an array-of-pointers layer since there is only one row)
|
||||
* and signal reassembly complete immediately -- returning here skips
|
||||
* the shared ntotal/pos-based tail below, which does not apply to a
|
||||
* single-shot payload. */
|
||||
if (getdata->data.job_array == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
dataset->rows = (void*)getdata->data.job_array;
|
||||
dataset->nrows = 1;
|
||||
getdata->data.job_array = NULL;
|
||||
|
||||
return 1;
|
||||
default:
|
||||
rc = -EINVAL;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -176,6 +176,99 @@ static void test_init_reply_by_req_zeroes_reply(void)
|
|||
TEST_ASSERT_EQUAL_INT(0, reply.data_which);
|
||||
}
|
||||
|
||||
/* ── job-array-single-wal-record task 5.3b: extract_data() wire dispatch for
|
||||
* Data_data_jobArray ─────────────────────────────────────────────────────
|
||||
* extract_data() is the shared inbound-push reassembly step every receiver
|
||||
* (scheduler, API server) passes through inside vtask_entry() (xcectx.c)
|
||||
* before its own handler ever runs. Its switch had no case for
|
||||
* Data_data_jobArray -- it fell into `default: rc = -EINVAL`, and
|
||||
* vtask_entry()'s `if (callnext)` treats ANY nonzero return (including a
|
||||
* negative errno) as "reassembly complete, proceed" -- so the real payload
|
||||
* was silently freed and the receiver was handed an empty, zeroed
|
||||
* dataset_t{rows=NULL, nrows=0}. This test drives the REAL wire encode/
|
||||
* decode path (mirrors xcesched_push_jobarray()'s exact construction,
|
||||
* sched.c, and apiserver_push_jobarray()'s identical shape, apiserver.c) so
|
||||
* the payload extract_data() sees here is genuinely wire-decoded, not a
|
||||
* hand-built struct. */
|
||||
|
||||
static void test_extract_data_job_array_passes_through_intact(void)
|
||||
{
|
||||
job_submit_data_t submit;
|
||||
xce_job_submit_t job_array;
|
||||
xce_data_t data;
|
||||
xce_request_t request;
|
||||
xce_request_t decoded;
|
||||
io_packet_t *pkt;
|
||||
dataset_t dataset;
|
||||
int rc;
|
||||
xce_job_submit_t *got;
|
||||
|
||||
/* Arrange: build the exact wire shape xcesched_push_jobarray()/
|
||||
* apiserver_push_jobarray() send: OPCode_push / Request_body_pushmsg
|
||||
* carrying one Data.jobArray descriptor (base jobid, array_size, one
|
||||
* shared submit spec) -- never chunked, exactly one per commit. */
|
||||
memset(&submit, 0, sizeof(submit));
|
||||
submit.queue = "normal";
|
||||
submit.user = "tester";
|
||||
submit.mintasks = 1;
|
||||
submit.maxtasks = 1;
|
||||
submit.resreq = "type == local";
|
||||
|
||||
memset(&job_array, 0, sizeof(job_array));
|
||||
job_array.jobid = 70000;
|
||||
job_array.array_size = 12;
|
||||
job_array.submit = &submit;
|
||||
|
||||
memset(&data, 0, sizeof(data));
|
||||
data.table = Data_data_jobArray;
|
||||
data.data.job_array = &job_array;
|
||||
|
||||
memset(&request, 0, sizeof(request));
|
||||
request.opcode = OPCode_push;
|
||||
request.body_which = Request_body_pushmsg;
|
||||
request.body.pushmsg = &data;
|
||||
|
||||
pkt = (io_packet_t *)calloc(1, sizeof(*pkt));
|
||||
TEST_ASSERT_NOT_NULL(pkt);
|
||||
TEST_ASSERT_EQUAL_INT(1, xce_encode(&request, pkt));
|
||||
|
||||
memset(&decoded, 0, sizeof(decoded));
|
||||
TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, &decoded)); /* consumes/frees pkt */
|
||||
TEST_ASSERT_EQUAL_INT(Request_body_pushmsg, decoded.body_which);
|
||||
TEST_ASSERT_NOT_NULL(decoded.body.pushmsg);
|
||||
TEST_ASSERT_EQUAL_INT(Data_data_jobArray, decoded.body.pushmsg->table);
|
||||
|
||||
/* Act: this is exactly what vtask_entry() calls on every inbound push. */
|
||||
memset(&dataset, 0, sizeof(dataset));
|
||||
dataset.table = decoded.body.pushmsg->table;
|
||||
rc = extract_data(&dataset, decoded.body.pushmsg);
|
||||
|
||||
/* Assert: nonzero/"reassembly complete" (matches vtask_entry's
|
||||
* `if (callnext)` truthy contract -- a single, unchunked descriptor never
|
||||
* waits for more) AND the genuine payload, not an empty dataset. Before
|
||||
* the fix, rc was -EINVAL (still truthy, so vtask_entry proceeded
|
||||
* anyway) but dataset.rows stayed NULL/nrows stayed 0 -- exactly the
|
||||
* silent-drop bug this test pins down. */
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_NOT_NULL(dataset.rows);
|
||||
TEST_ASSERT_EQUAL_INT(1, dataset.nrows);
|
||||
|
||||
got = (xce_job_submit_t *)dataset.rows;
|
||||
TEST_ASSERT_EQUAL_UINT64(70000, got->jobid);
|
||||
TEST_ASSERT_EQUAL_UINT64(12, got->array_size);
|
||||
TEST_ASSERT_NOT_NULL(got->submit);
|
||||
TEST_ASSERT_EQUAL_STRING("normal", got->submit->queue);
|
||||
TEST_ASSERT_EQUAL_STRING("tester", got->submit->user);
|
||||
|
||||
/* Cleanup: extract_data() transfers ownership of the decoded
|
||||
* xce_job_submit_t out of decoded.body.pushmsg->data.job_array into
|
||||
* dataset.rows (nulling the source pointer) -- free the now-hollow
|
||||
* container and the transferred descriptor separately. */
|
||||
xce_free_request(&decoded);
|
||||
free_JobSubmit_ptr((xce_job_submit_t **)&dataset.rows);
|
||||
printf("test_extract_data_job_array_passes_through_intact: PASS\n");
|
||||
}
|
||||
|
||||
void test_request(void)
|
||||
{
|
||||
printf("\n>> host control request validation tests\n\n");
|
||||
|
|
@ -193,4 +286,7 @@ void test_request(void)
|
|||
RUN_TEST(test_ha_heartbeat_req_null_ep);
|
||||
RUN_TEST(test_ha_heartbeat_req_roundtrip_fields);
|
||||
RUN_TEST(test_nop_req_remains_bodyless);
|
||||
|
||||
printf("\n>> extract_data() Data_data_jobArray wire dispatch tests\n\n");
|
||||
RUN_TEST(test_extract_data_job_array_passes_through_intact);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,6 +248,102 @@ static void test_job_array_null_descriptor_returns_einval(void)
|
|||
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_on_job_array(&sched, NULL));
|
||||
}
|
||||
|
||||
/* ── job-array-single-wal-record task 5.3b: full wire-dispatch chain ──────
|
||||
* Every test above feeds a stack-built xce_job_submit_t straight into
|
||||
* sched_handle_dataset(), bypassing the transport layer entirely -- the same
|
||||
* white-box convention every sibling handler.c test uses (task 5.3's own
|
||||
* Implementation Notes call this out explicitly: those tests were never
|
||||
* intended to prove real-wire delivery). This test drives the REAL
|
||||
* xce_encode()/xce_decode()/extract_data() path (src/lib/request.c, task
|
||||
* 5.3b's fix) with the exact wire shape xcesched_push_jobarray() (sched.c)
|
||||
* sends -- OPCode_push / Request_body_pushmsg carrying one Data.jobArray
|
||||
* descriptor -- then hands extract_data()'s reassembled dataset_t to
|
||||
* sched_handle_dataset(), proving the sender-payload -> wire-dispatch ->
|
||||
* receiver-expand chain is intact end to end, not just sched_on_job_array()
|
||||
* in isolation. */
|
||||
|
||||
static void test_wire_dispatch_chain_delivers_n_tasks(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
job_submit_data_t submit;
|
||||
xce_job_submit_t job_array;
|
||||
xce_data_t data;
|
||||
xce_request_t request;
|
||||
xce_request_t decoded;
|
||||
io_packet_t *pkt;
|
||||
dataset_t dataset;
|
||||
xce_job_t **jobs = NULL;
|
||||
int njobs;
|
||||
int i;
|
||||
|
||||
fixture_init(&sched);
|
||||
|
||||
/* Arrange: the exact wire message xcesched_push_jobarray() sends. */
|
||||
memset(&submit, 0, sizeof(submit));
|
||||
submit.queue = "normal";
|
||||
submit.user = "tester";
|
||||
submit.mintasks = 1;
|
||||
submit.maxtasks = 1;
|
||||
submit.resreq = "type == local";
|
||||
|
||||
memset(&job_array, 0, sizeof(job_array));
|
||||
job_array.jobid = 8000;
|
||||
job_array.array_size = 4;
|
||||
job_array.submit = &submit;
|
||||
|
||||
memset(&data, 0, sizeof(data));
|
||||
data.table = Data_data_jobArray;
|
||||
data.data.job_array = &job_array;
|
||||
|
||||
memset(&request, 0, sizeof(request));
|
||||
request.opcode = OPCode_push;
|
||||
request.body_which = Request_body_pushmsg;
|
||||
request.body.pushmsg = &data;
|
||||
|
||||
pkt = (io_packet_t *)calloc(1, sizeof(*pkt));
|
||||
TEST_ASSERT_NOT_NULL(pkt);
|
||||
TEST_ASSERT_EQUAL_INT(1, xce_encode(&request, pkt));
|
||||
|
||||
memset(&decoded, 0, sizeof(decoded));
|
||||
TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, &decoded)); /* consumes/frees pkt */
|
||||
|
||||
/* Act 1: the REAL transport dispatcher (task 5.3b's fix) -- exactly what
|
||||
* vtask_entry() (xcectx.c) calls on every inbound push. */
|
||||
memset(&dataset, 0, sizeof(dataset));
|
||||
dataset.table = decoded.body.pushmsg->table;
|
||||
TEST_ASSERT_EQUAL_INT(1, extract_data(&dataset, decoded.body.pushmsg));
|
||||
TEST_ASSERT_NOT_NULL(dataset.rows);
|
||||
|
||||
/* Act 2: hand the reassembled dataset straight to the receiver's own
|
||||
* handler (task 5.3, unmodified) -- proving the two layers compose. */
|
||||
TEST_ASSERT_TRUE(sched_handle_dataset(&sched, &dataset) >= 0);
|
||||
|
||||
/* Assert: N schedulable tasks, correctly addressed via base_jobid + i. */
|
||||
njobs = pop_job_update(&sched, &jobs);
|
||||
TEST_ASSERT_EQUAL_INT(4, njobs);
|
||||
TEST_ASSERT_EQUAL_UINT64(8000, jobs[0]->jobid);
|
||||
TEST_ASSERT_EQUAL_UINT64(8001, jobs[1]->jobid);
|
||||
TEST_ASSERT_EQUAL_UINT64(8002, jobs[2]->jobid);
|
||||
TEST_ASSERT_EQUAL_UINT64(8003, jobs[3]->jobid);
|
||||
TEST_ASSERT_EQUAL_STRING("normal", jobs[0]->submit->queue);
|
||||
TEST_ASSERT_EQUAL_STRING("normal", jobs[3]->submit->queue);
|
||||
|
||||
for (i = 0; i < njobs; i++) {
|
||||
free_Job_ptr(&jobs[i]);
|
||||
}
|
||||
free(jobs);
|
||||
|
||||
/* 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
|
||||
* hollow container, safe to free via the normal request teardown. */
|
||||
xce_free_request(&decoded);
|
||||
free_JobSubmit_ptr((xce_job_submit_t **)&dataset.rows);
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
(void)argc; (void)argv;
|
||||
|
|
@ -257,5 +353,6 @@ int main(int argc, char* argv[])
|
|||
RUN_TEST(test_job_array_null_submit_returns_einval, 0);
|
||||
RUN_TEST(test_job_array_zero_size_returns_einval, 0);
|
||||
RUN_TEST(test_job_array_null_descriptor_returns_einval, 0);
|
||||
RUN_TEST(test_wire_dispatch_chain_delivers_n_tasks, 0);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue