controller.capnp declared `struct Data $C.mapname("xce_data_t")` (an
OPCode_get query-result tagged union) at file scope, which shadows
Cap'n Proto's builtin Data blob type for every later `: Data` field in
the same file. This silently miscompiled 7 fields across 6 structs as
a struct-pointer type instead of a raw byte blob: UpgradeConfigRequest
.sha256/.bytes, ReplConfigReply.sha256/.bytes, ReplConfigPushRequest
.sha256/.bytes, ReplEnvelope.payload, SnapshotTransfer's chunk
payload, WalCatchupEntry.payload/.batchPayloads, and CtrlHelloRequest
.key. Production C code had already routed around it for years via
hand-rolled raw capn_ptr manipulation (see request.c's own comments);
nothing before now needed the generated codec/accessors to actually
work for these fields.
Renamed the local struct to XceData (capnp-level name only; the
$C.mapname("xce_data_t") C typedef is unchanged) and explicitly
repointed getdata/pushmsg at XceData by name, since those two fields
correctly want the local tagged union. Updated every derived
generated-identifier reference across 19 hand-written files spanning
controller, lib, scheduler, and the Go apiserver/xcecli CGo bindings.
Fixed 5 call sites in lib/request.c that had been typed against the
old workaround alias but now legitimately receive the correct
capn_data type (a pure type-name swap -- Data_ptr/XceData_ptr and
capn_data share the identical {capn_ptr p} layout).
Verified via full rebuild+test: lib (1/1), controller (3/3, incl. the
job-array push regression tests), scheduler (15/15), Go apiserver +
xcecli (all packages green), and a direct pycapnp round-trip proving
UpgradeConfigRequest.sha256/.bytes now encode/decode correctly.
Bug: bug-1461 (.wolf/buglog.json). Unblocks admin-config-tool task 3.3.
335 lines
12 KiB
C
335 lines
12 KiB
C
/* test_job_array_apiserver_push.c — job-array-single-wal-record task 5.2
|
|
*
|
|
* Unit tests for the Compact Scheduler/API-Server Push component's
|
|
* controller-side API-server push (design.md "Compact Scheduler/API-Server
|
|
* Push"; Req 6.2, 6.4), mirroring task 5.1's test_job_array_sched_push.c
|
|
* construction for sched.c's xcesched_push_jobarray() -- same discriminator
|
|
* (l->count > 1), same direct-xce_request() transport -- but for
|
|
* apiserver.c's apiserver_job_new_fun()/apiserver_push_joblist().
|
|
*
|
|
* Fan-out difference from sched.c: sched.c has a single c->sched.ep, so
|
|
* task 5.1 could only prove "one push per notification". apiserver.c
|
|
* already fans a push out over an array of connected servers
|
|
* (c->apiservers[N_APISERVER_MAX]), exactly like apiserver_push_joblist()'s
|
|
* existing per-server loop -- so this file simulates TWO connected API
|
|
* servers to prove the new compact push is genuinely per-connected-server,
|
|
* not just per-notification (task 5.2's explicit observable: "exactly one
|
|
* compact push PER CONNECTED API SERVER").
|
|
*
|
|
* - A synthetic multi-element HOOK_DETAIL_JOB_NEW fire (the exact shape
|
|
* apply_committed_entry()'s WAL_JOB_ARRAY_SUBMIT case produces, task 3.2
|
|
* -- one AFTER_EVENT fire, one xlist_t carrying every constructed
|
|
* element) produces EXACTLY ONE compact Data.jobArray push on EACH of
|
|
* the two connected servers' endpoints (two pushes total) -- never N
|
|
* individually-encoded Job structs, and never fewer than one push per
|
|
* connected server.
|
|
* - A synthetic single-element fire (ordinary single-job submission) is
|
|
* byte-for-byte unaffected on every connected server: still the
|
|
* existing Data.jobs push via apiserver_push_joblist(), unchanged
|
|
* (Req 8.1).
|
|
*
|
|
* Fixture: apiserver_job_new_fun()/apiserver_push_joblist()/
|
|
* apiserver_push_jobarray() are static to apiserver.c, so this file drives
|
|
* them through the same public entry point production code uses:
|
|
* apiserver_init() installs the HOOK_DETAIL_JOB_NEW hook (guarded locally
|
|
* with a static flag -- unlike sched.c's on_scheduler_join(),
|
|
* apiserver_init() has no idempotency guard of its own, so calling it twice
|
|
* would double-register hooks and double-fire pushes) and xce_run_hooks()
|
|
* fires it exactly as apply_committed_entry() does. Each connected server's
|
|
* outbound push is observed the same way test_job_array_sched_push.c
|
|
* observes them: a zeroed, fd=-1 stack endpoint_t queues packets on
|
|
* ep->outq without touching a real socket (endpoint_update_epoll no-ops,
|
|
* zeroed pthread mutexes are valid); this test drains and raw-capnp-decodes
|
|
* those packets.
|
|
*
|
|
* Test-binary strategy (verified against this task's own dependency graph,
|
|
* NOT assumed identical to sched.c's): apiserver.c calls notify(),
|
|
* collect_resval(), and check_controller() -- all defined in dataex.c, the
|
|
* same real-vs-fake collision task 5.1 hit (test_commit_barrier_reply.c and
|
|
* test_worker_join.c already define their OWN fake notify()/
|
|
* on_scheduler_join(), so linking the real dataex.c into the shared
|
|
* controller_unit_test binary collides at link time). UNLIKE sched.c,
|
|
* apiserver.c never calls job_submit_committed()/collect_jobs_committed()
|
|
* (egress.c) -- apiserver_push_joblist() has no WAL-commit filter, a
|
|
* known, pre-existing, out-of-scope gap (see apiserver.c's own history) --
|
|
* so this standalone binary links apiserver.c + dataex.c only, WITHOUT
|
|
* egress.c (confirmed unnecessary by grep, not assumed). Mirrors
|
|
* sched_push_unit_test's standalone-binary strategy (meson.build).
|
|
*/
|
|
|
|
#include <capnp_c.h>
|
|
#include <errno.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "cluster.h"
|
|
#include "controller.capnp.h"
|
|
#include "hook.h"
|
|
#include "netio.h"
|
|
#include "request.h"
|
|
#include "xce/cluster_def.h"
|
|
#include "xce/types.h"
|
|
#include "xlist.h"
|
|
|
|
extern int apiserver_init(cluster_t*);
|
|
|
|
/* apiserver.c's xce_apiserver_push_workers() (reached only through
|
|
* on_apiserver_join(), which this file's fixture never calls -- see
|
|
* ap_setup) references opget.c's check_worker_() as a callback function
|
|
* pointer, so the symbol must still resolve at link time even though it is
|
|
* never invoked at runtime here. Stubbed locally rather than linking all of
|
|
* opget.c, mirroring test_job_array_sched_push.c's identical stub for the
|
|
* identical reason (sched.c's xcesched_push_workers() has the same
|
|
* reference). */
|
|
int check_worker_(xce_worker_t* w) { return (w != NULL) ? 1 : 0; }
|
|
|
|
#define APISRV_TEST_N 2
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Fixture */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static cluster_t g_ap_c;
|
|
static endpoint_t g_ap_ep[APISRV_TEST_N];
|
|
static int g_ap_hook_wired;
|
|
|
|
static void ap_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 ap_setup(void)
|
|
{
|
|
int i;
|
|
|
|
if (!g_ap_hook_wired) {
|
|
memset(&g_ap_c, 0, sizeof(g_ap_c));
|
|
TEST_ASSERT_EQUAL_INT(
|
|
0, xce_hook_init(&g_ap_c.hooksys, XCE_CONTROLLER_HOOK_MAX));
|
|
/* apiserver_init() mylock_inits every c->apiservers[] slot AND
|
|
* installs the HOOK_DETAIL_JOB_NEW (+ sibling) hooks. It has no own
|
|
* idempotency guard (unlike on_scheduler_join()), so this file
|
|
* guards the call itself: exactly once, process-wide, shared by
|
|
* every test below. */
|
|
TEST_ASSERT_EQUAL_INT(0, apiserver_init(&g_ap_c));
|
|
g_ap_hook_wired = 1;
|
|
}
|
|
|
|
for (i = 0; i < APISRV_TEST_N; i++) {
|
|
memset(&g_ap_ep[i], 0, sizeof(g_ap_ep[i]));
|
|
g_ap_ep[i].fd = -1;
|
|
|
|
g_ap_c.apiservers[i].name = (char*)"apisrv-jsp";
|
|
g_ap_c.apiservers[i].ep = &g_ap_ep[i];
|
|
}
|
|
for (i = APISRV_TEST_N; i < N_APISERVER_MAX; i++) {
|
|
g_ap_c.apiservers[i].name = NULL;
|
|
g_ap_c.apiservers[i].ep = NULL;
|
|
}
|
|
|
|
g_ap_c.running = 1;
|
|
g_ap_c.myhost = &g_ap_c.myhost_info;
|
|
g_ap_c.myhost_info.name = (char*)"ctrl-jsp";
|
|
g_ap_c.myhost_info.key = (char*)"";
|
|
}
|
|
|
|
static void ap_teardown(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < APISRV_TEST_N; i++) {
|
|
ap_drain_ep(&g_ap_ep[i]);
|
|
g_ap_c.apiservers[i].name = NULL;
|
|
g_ap_c.apiservers[i].ep = NULL;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static job_submit_data_t g_ap_submit_data;
|
|
|
|
/* apiserver_push_joblist()/apiserver_push_jobarray() only ever read
|
|
* xce_job_t's own .jobid/.submit fields directly (never CONTAINER_OF into a
|
|
* wrapping job_t, unlike sched.c's job_submit_committed() filter) -- so
|
|
* plain xce_job_t elements are the correct, minimal fixture here. */
|
|
static void ap_make_job(xce_job_t* out, uint64_t jobid)
|
|
{
|
|
memset(out, 0, sizeof(*out));
|
|
out->jobid = jobid;
|
|
out->submit = &g_ap_submit_data;
|
|
}
|
|
|
|
static int ap_fire_job_new(xlist_t* l)
|
|
{
|
|
return xce_run_hooks(&g_ap_c.hooksys, XCE_CONTROLLER_HOOK_JOB,
|
|
HOOK_AFTER_EVENT, HOOK_DETAIL_JOB_NEW, &g_ap_c, l);
|
|
}
|
|
|
|
typedef struct {
|
|
int n_pushes;
|
|
int last_data_which;
|
|
uint64_t last_jobid;
|
|
uint64_t last_array_size;
|
|
} ap_push_obs_t;
|
|
|
|
/* Raw-capnp-parse every queued push packet on ep->outq (mirrors
|
|
* test_job_array_sched_push.c's jsp_scan_pushes). Drains the queue. */
|
|
static void ap_scan_pushes(endpoint_t* ep, ap_push_obs_t* obs)
|
|
{
|
|
data_item_t* it;
|
|
|
|
memset(obs, 0, sizeof(*obs));
|
|
while ((it = endpoint_deq(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_pushmsg) {
|
|
struct XceData d;
|
|
|
|
read_XceData(&d, rq.body.pushmsg);
|
|
obs->n_pushes += 1;
|
|
obs->last_data_which = (int)d.data_which;
|
|
if (d.data_which == XceData_data_jobArray) {
|
|
struct JobSubmit js;
|
|
|
|
read_JobSubmit(&js, d.data.jobArray);
|
|
obs->last_jobid = js.jobid;
|
|
obs->last_array_size = js.arraySize;
|
|
}
|
|
}
|
|
capn_free(&c);
|
|
free_packet(pkt);
|
|
free(pkt);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test 1: multi-element fire -> one compact jobArray push PER connected */
|
|
/* API server */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void
|
|
test_multi_element_job_new_produces_one_compact_jobarray_push_per_server(void)
|
|
{
|
|
xce_job_t jobs[5];
|
|
void* storage[5];
|
|
xlist_t l = { .count = 5, .caps = 5, .delta = 5, .data = storage };
|
|
ap_push_obs_t obs;
|
|
int rc;
|
|
int i;
|
|
|
|
ap_setup();
|
|
|
|
for (i = 0; i < 5; i++) {
|
|
ap_make_job(&jobs[i], 6000 + (uint64_t)i);
|
|
storage[i] = &jobs[i];
|
|
}
|
|
|
|
rc = ap_fire_job_new(&l);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
for (i = 0; i < APISRV_TEST_N; i++) {
|
|
ap_scan_pushes(&g_ap_ep[i], &obs);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, obs.n_pushes,
|
|
"a 5-element array notification must produce exactly ONE push "
|
|
"packet per connected API server, not N individually-encoded "
|
|
"job structs and not fewer than one per connected server (Req "
|
|
"6.2, 6.4)");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
XceData_data_jobArray, obs.last_data_which,
|
|
"the push must encode the compact Data.jobArray descriptor, not "
|
|
"Data.jobs (N individually-encoded job structs)");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
|
6000, obs.last_jobid,
|
|
"base jobid must be the array's first (lowest) jobid");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
|
5, obs.last_array_size,
|
|
"arraySize must be the notification's element count");
|
|
}
|
|
|
|
ap_teardown();
|
|
printf(
|
|
"test_multi_element_job_new_produces_one_compact_jobarray_push_per_"
|
|
"server: PASS\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Test 2: single-element fire -> unmodified per-job Data.jobs push, */
|
|
/* still fanned out to every connected server */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void test_single_element_job_new_is_unaffected(void)
|
|
{
|
|
xce_job_t job;
|
|
void* storage[1];
|
|
xlist_t l = { .count = 1, .caps = 1, .delta = 1, .data = storage };
|
|
ap_push_obs_t obs;
|
|
int rc;
|
|
int i;
|
|
|
|
ap_setup();
|
|
|
|
ap_make_job(&job, 9101);
|
|
storage[0] = &job;
|
|
|
|
rc = ap_fire_job_new(&l);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
for (i = 0; i < APISRV_TEST_N; i++) {
|
|
ap_scan_pushes(&g_ap_ep[i], &obs);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, obs.n_pushes,
|
|
"an ordinary single-job notification must still produce exactly "
|
|
"one push packet per connected API server, unchanged");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
XceData_data_jobs, obs.last_data_which,
|
|
"a single-element notification must go through the EXISTING, "
|
|
"UNMODIFIED apiserver_push_joblist() path (Data.jobs), never "
|
|
"the new compact jobArray path (Req 8.1)");
|
|
}
|
|
|
|
ap_teardown();
|
|
printf("test_single_element_job_new_is_unaffected: PASS\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Standalone Unity main (this binary is its own test executable -- see */
|
|
/* meson.build's apiserver_push_unit_test) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
int main(void)
|
|
{
|
|
printf("\n>> job-array-single-wal-record task 5.2: compact API-server "
|
|
"push\n\n");
|
|
|
|
UNITY_BEGIN();
|
|
|
|
RUN_TEST(
|
|
test_multi_element_job_new_produces_one_compact_jobarray_push_per_server);
|
|
RUN_TEST(test_single_element_job_new_is_unaffected);
|
|
|
|
return UNITY_END();
|
|
}
|