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:
Rongsong Shen 2026-09-01 19:43:18 +00:00
parent 48c6cfd58a
commit a3d12d37d9
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
8 changed files with 1045 additions and 3 deletions

View file

@ -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;
}

View file

@ -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(