feat(worker-rejoin-resync): task 6.1 - trigger reconciliation on rejoin
Arm a reconciliation round in apply_join_state() the moment a worker host becomes connected (first join, reconnect, or failover-reconnect - all routed through the same shared call site, Req 1.1-1.3), then send the query from on_worker_join() strictly after the join reply and existing config push (Req 2.1-2.2), verified via a structural test that reads real wire order off the endpoint queue. Also closes Req 3.7's config-delivery seam end-to-end: worker_resync_ window_ms now rides the existing Conf4Worker push (clustername precedent, no new opcode) and the worker applies it to its retention window on receipt. Fixes bug-1752: myreq_handler had no OPCode_push dispatch case at all, so every config push - including the pre-existing clustername field - was silently discarded. Confirmed harmless until now (clustername was never read anywhere in src/worker/); closed narrowly for worker_resync_window_ms only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4M5MJM2zK6nU43da267Ku
This commit is contained in:
parent
48c6cfd58a
commit
a3d12d37d9
8 changed files with 1045 additions and 3 deletions
|
|
@ -1663,6 +1663,54 @@ static int handle_jobstatequery(xce_request_t* request)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles an incoming OPCode_push config-push message from the
|
||||
* controller (worker-rejoin-resync task 6.1, Requirement 3.7).
|
||||
*
|
||||
* Confirmed by inspection: before this task, myreq_handler's dispatch
|
||||
* switch below had NO case for OPCode_push at all -- a config push
|
||||
* (worker_push_config()/on_worker_join(), controller/worker.c) fell through
|
||||
* to the `default: break;` case and was silently discarded in its
|
||||
* entirety, even for the pre-existing clustername field.
|
||||
* xcecd_finished_job_retention_set_window_ms()'s own doc comment
|
||||
* (xcecd_worker_utils.h, task 4.1) independently confirms this same gap:
|
||||
* "the worker daemon has no runtime delivery path for this ClusterConfig
|
||||
* field today... this setter is the seam a later task wires the real
|
||||
* operator-configured value into once that delivery path exists." This
|
||||
* function is that delivery path, scoped narrowly to the ONE field this
|
||||
* task needs (workerResyncWindowMs) -- clustername/confvars remain
|
||||
* unconsumed by the worker, unchanged and out of this task's boundary; a
|
||||
* general config-receive rework is not attempted here.
|
||||
*
|
||||
* Fire-and-forget, matching worker_push_config()'s own controller-side
|
||||
* send (no reply is expected or sent back).
|
||||
*
|
||||
* @param request Pointer to the xce_request_t structure containing the
|
||||
* request data.
|
||||
* @return int 0 always -- a malformed/unexpected payload shape (wrong
|
||||
* body_which, wrong ConfigData union arm, NULL conf4worker) is
|
||||
* simply ignored, not an error to report back.
|
||||
*/
|
||||
static int handle_pushconfig(xce_request_t* request)
|
||||
{
|
||||
xce_configdata_t* configdata;
|
||||
|
||||
if (request->body_which != Request_body_configpushmsg)
|
||||
return 0;
|
||||
|
||||
configdata = request->body.configpushmsg;
|
||||
if (configdata == NULL
|
||||
|| configdata->data_which != ConfigData_data_workerconfig
|
||||
|| configdata->data.conf4worker == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
xcecd_finished_job_retention_set_window_ms(
|
||||
(uint64_t)configdata->data.conf4worker->worker_resync_window_ms);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles incoming requests from the controller.
|
||||
*
|
||||
|
|
@ -1691,6 +1739,9 @@ static int myreq_handler(xce_request_t* request, void* args)
|
|||
case OPCode_jobStateQuery:
|
||||
ret = handle_jobstatequery(request);
|
||||
break;
|
||||
case OPCode_push:
|
||||
ret = handle_pushconfig(request);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,23 @@ static const xce_job_state_query_t* g_resync_handle_query_page_last_page = NULL;
|
|||
* touching it directly. */
|
||||
static int g_resync_reset_call_count = 0;
|
||||
|
||||
/* worker-rejoin-resync task 6.1 (Requirement 3.7): link-satisfying spy stub
|
||||
* for xcecd_worker_utils.c's real retention-window setter -- this file's
|
||||
* white-box target is controller_connector.c only (does not #include
|
||||
* xcecd_worker_utils.c), same rationale as the xcecd_resync.c stubs above.
|
||||
* Lets the dispatch-wiring tests below observe that handle_pushconfig()
|
||||
* (myreq_handler's new OPCode_push case) actually reaches this function
|
||||
* with the decoded value, without needing a real xcecd_worker_utils.c
|
||||
* retention store backing it. */
|
||||
static int g_retention_set_call_count;
|
||||
static uint64_t g_retention_window_ms_set;
|
||||
|
||||
void xcecd_finished_job_retention_set_window_ms(uint64_t window_ms)
|
||||
{
|
||||
g_retention_set_call_count++;
|
||||
g_retention_window_ms_set = window_ms;
|
||||
}
|
||||
|
||||
bool xcecd_resync_is_pending(void) { return g_test_resync_pending; }
|
||||
|
||||
void xcecd_resync_reset(void)
|
||||
|
|
@ -201,6 +218,10 @@ void setUp(void)
|
|||
g_resync_handle_query_page_call_count = 0;
|
||||
g_resync_handle_query_page_last_page = NULL;
|
||||
g_resync_reset_call_count = 0;
|
||||
|
||||
/* worker-rejoin-resync task 6.1: reset the retention-window setter spy. */
|
||||
g_retention_set_call_count = 0;
|
||||
g_retention_window_ms_set = 0;
|
||||
}
|
||||
|
||||
void tearDown(void) { return; }
|
||||
|
|
@ -902,6 +923,160 @@ test_myreq_handler_jobstatequery_without_connection_is_rejected(void)
|
|||
"PASS\n");
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
* worker-rejoin-resync task 6.1 (Requirement 3.7): myreq_handler's new
|
||||
* OPCode_push case (handle_pushconfig) must apply a received
|
||||
* workerResyncWindowMs value to the retention window before the push
|
||||
* completes.
|
||||
*
|
||||
* Before this task, myreq_handler had NO case for OPCode_push at all --
|
||||
* confirmed by inspection (this file's own white-box #include of the real,
|
||||
* current controller_connector.c) -- so a config push fell through to
|
||||
* `default: break;` and was silently discarded in its entirety, even for
|
||||
* the pre-existing clustername field. xcecd_finished_job_retention_set_
|
||||
* window_ms()'s own doc comment (xcecd_worker_utils.h, task 4.1)
|
||||
* independently confirms the same gap. This is the first test coverage of
|
||||
* that dispatch case existing at all.
|
||||
* ════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Builds a real, wire-encoded OPCode_push carrying a Conf4Worker payload
|
||||
* with the given workerResyncWindowMs value, pops it back off the queue,
|
||||
* and decodes it with the real xce_decode() -- exercising the actual
|
||||
* capnp encode/decode round trip (controller.capnp.c, generated), not just
|
||||
* a directly-constructed xce_request_t. Mirrors src/lib/testsuite/
|
||||
* test_job_state_wire.c's own real-endpoint round-trip pattern. Caller
|
||||
* must xce_free_request(out) and endpoint_fini()/free() the endpoint. */
|
||||
static endpoint_t*
|
||||
build_and_decode_pushconfig(uint32_t worker_resync_window_ms,
|
||||
xce_request_t* out)
|
||||
{
|
||||
xce_conf4worker_t conf4worker = { 0 };
|
||||
conf4worker.clustername = (char*)"test-cluster";
|
||||
conf4worker.worker_resync_window_ms = worker_resync_window_ms;
|
||||
|
||||
xce_configdata_t configdata = { 0 };
|
||||
configdata.data_which = ConfigData_data_workerconfig;
|
||||
configdata.data.conf4worker = &conf4worker;
|
||||
|
||||
char nm[] = "test-controller";
|
||||
char ky[] = "test-key";
|
||||
xce_peerinfo_t from = { .name = nm, .key = ky };
|
||||
|
||||
endpoint_t* ep = endpoint_get();
|
||||
TEST_ASSERT_NOT_NULL(ep);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, xce_pushconfig_req(ep, &from, &configdata));
|
||||
|
||||
data_item_t* it = endpoint_deq(ep);
|
||||
TEST_ASSERT_NOT_NULL(it);
|
||||
io_packet_t* pkt = ITPTR_TO_PACKET(it);
|
||||
|
||||
memset(out, 0, sizeof(*out));
|
||||
TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, out));
|
||||
|
||||
return ep;
|
||||
}
|
||||
|
||||
/* A config push carrying a non-default workerResyncWindowMs value must be
|
||||
* applied to the retention window -- synchronously, within myreq_handler's
|
||||
* own call, i.e. before the push "completes" from the caller's point of
|
||||
* view (Requirement 3.7's own observable completion criterion). */
|
||||
static void
|
||||
test_myreq_handler_routes_pushconfig_workerconfig_to_retention_setter(void)
|
||||
{
|
||||
xce_request_t decoded;
|
||||
endpoint_t* ep = build_and_decode_pushconfig(4242, &decoded);
|
||||
|
||||
int rc = myreq_handler(&decoded, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, g_retention_set_call_count,
|
||||
"OPCode_push carrying a Conf4Worker payload must reach "
|
||||
"xcecd_finished_job_retention_set_window_ms() exactly once -- the "
|
||||
"dispatch case task 6.1 adds (no case existed for OPCode_push "
|
||||
"before this task)");
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
||||
4242, g_retention_window_ms_set,
|
||||
"the applied value must be the real value decoded off the wire, "
|
||||
"not a hardcoded/default one");
|
||||
|
||||
xce_free_request(&decoded);
|
||||
endpoint_fini(ep);
|
||||
free(ep);
|
||||
printf("test_myreq_handler_routes_pushconfig_workerconfig_to_retention_"
|
||||
"setter: PASS\n");
|
||||
}
|
||||
|
||||
/* A second, different value must also be applied -- confirms the setter is
|
||||
* called with the field genuinely read from each push, not a fixed
|
||||
* constant from the first call. */
|
||||
static void
|
||||
test_myreq_handler_routes_pushconfig_second_value_also_applied(void)
|
||||
{
|
||||
xce_request_t decoded;
|
||||
endpoint_t* ep = build_and_decode_pushconfig(7, &decoded);
|
||||
|
||||
int rc = myreq_handler(&decoded, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, g_retention_set_call_count);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, g_retention_window_ms_set);
|
||||
|
||||
xce_free_request(&decoded);
|
||||
endpoint_fini(ep);
|
||||
free(ep);
|
||||
printf("test_myreq_handler_routes_pushconfig_second_value_also_applied: "
|
||||
"PASS\n");
|
||||
}
|
||||
|
||||
/* A config push for a DIFFERENT ConfigData union arm (e.g. a scheduler's
|
||||
* Conf4Sched, or the raw ClusterConfig arm) must NOT be misread as a
|
||||
* Conf4Worker payload -- the retention setter must not fire at all. */
|
||||
static void test_myreq_handler_pushconfig_ignores_non_workerconfig_data(void)
|
||||
{
|
||||
cluster_config_t clusterconfig = { 0 };
|
||||
clusterconfig.clustername = (char*)"test-cluster";
|
||||
|
||||
xce_configdata_t configdata = { 0 };
|
||||
configdata.data_which = ConfigData_data_clusterconfig;
|
||||
configdata.data.clusterconfig = &clusterconfig;
|
||||
|
||||
xce_request_t request = { 0 };
|
||||
request.opcode = OPCode_push;
|
||||
request.body_which = Request_body_configpushmsg;
|
||||
request.body.configpushmsg = &configdata;
|
||||
|
||||
int rc = myreq_handler(&request, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, g_retention_set_call_count,
|
||||
"a non-workerconfig ConfigData arm must never reach the retention "
|
||||
"setter");
|
||||
|
||||
printf("test_myreq_handler_pushconfig_ignores_non_workerconfig_data: "
|
||||
"PASS\n");
|
||||
}
|
||||
|
||||
/* A NULL configpushmsg (defensive: should never happen for a genuinely
|
||||
* decoded OPCode_push, but the handler must not crash) must also be a
|
||||
* silent no-op. */
|
||||
static void test_myreq_handler_pushconfig_ignores_null_payload(void)
|
||||
{
|
||||
xce_request_t request = { 0 };
|
||||
request.opcode = OPCode_push;
|
||||
request.body_which = Request_body_configpushmsg;
|
||||
request.body.configpushmsg = NULL;
|
||||
|
||||
int rc = myreq_handler(&request, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, g_retention_set_call_count);
|
||||
|
||||
printf("test_myreq_handler_pushconfig_ignores_null_payload: PASS\n");
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
* Task 2.3 — structural regression guard: xcecli_set_reconnect_handler()
|
||||
* must be called AFTER xcecli_start() succeeds, never before.
|
||||
|
|
@ -1252,6 +1427,11 @@ int main(void)
|
|||
test_checking_connection_survives_permanently_stuck_resync_pending);
|
||||
RUN_TEST(test_myreq_handler_routes_jobstatequery_to_resync_module);
|
||||
RUN_TEST(test_myreq_handler_jobstatequery_without_connection_is_rejected);
|
||||
RUN_TEST(
|
||||
test_myreq_handler_routes_pushconfig_workerconfig_to_retention_setter);
|
||||
RUN_TEST(test_myreq_handler_routes_pushconfig_second_value_also_applied);
|
||||
RUN_TEST(test_myreq_handler_pushconfig_ignores_non_workerconfig_data);
|
||||
RUN_TEST(test_myreq_handler_pushconfig_ignores_null_payload);
|
||||
RUN_TEST(
|
||||
test_reconnect_handler_registered_after_start_not_before_in_source);
|
||||
RUN_TEST(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue