fix(esd): re-join after transparent reconnect -- bug-1978

A controller restart dropped xceesd's resval pushes forever: the fresh
endpoint is created by handle_connection() with priv->role =
HostRole_client, and only OPCode_join sets HostRole_extres -- but the
old esd_on_reconnect cleared only g_needs_full_push, so the daemon
re-PUSHED onto an un-joined endpoint and every push landed in
do_update_resvals's neither-branch, refused -EPERM forever (live
sighting: pushes_ok frozen, rc=-1 every 10s).

Mirror the worker's needs_rejoin contract: the reconnect handler now
sets BOTH g_needs_rejoin and g_needs_full_push; the main loop consumes
the re-join first (xcecli_join(HostRole_extres) on the main thread,
outside the failover lock -- the xcecd_on_reconnect no-I/O constraint),
retrying next tick on failure, then does the full re-push.

Live t1/t2 proof: controller restart -> "esd: re-joined after
reconnect (rc=0)" + "full re-push: 1/1 source(s)" + extlic back in
`xcecli resources distribution list`. Two new white-box unit tests pin
the both-flags contract (13/13 in the esd suite).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-10-28 14:07:28 +00:00
parent b38ef6de63
commit b471ce31ca
5 changed files with 219 additions and 7 deletions

View file

@ -26,6 +26,13 @@
/* White-box: the stale predicate and the status-file writer (Phase 2,
* design.md 3.5 "运维面"). */
#include "../src/esd_status.c"
/* White-box: the reconnect-flag contract (bug-1978). esd.c's main loop
* drives the live client library, but the reconnect handler's flag contract
* -- set BOTH rejoin and full-push, and the loop consuming rejoin first --
* is unit-testable here without a controller: esd_run() itself is not
* linked (its join sequence would need a live primary); we drive
* esd_on_reconnect and read the loop's guard statics directly. */
#include "../src/esd.c"
static char g_tmpdir[256];
@ -373,6 +380,38 @@ static void test_status_write_marks_stale(void)
free(cfg.src);
}
/* ── bug-1978: the reconnect-flag contract ───────────────────────────── */
static void test_reconnect_sets_both_rejoin_and_full_push(void)
{
g_needs_rejoin = 0;
g_needs_full_push = 0;
TEST_ASSERT_EQUAL_INT(0, esd_on_reconnect((xcecli_ctx_t)0x1, NULL));
/* BOTH flags: the re-join must happen before any re-push (an un-joined
* push is refused -EPERM on the fresh endpoint), and the re-push must
* still happen (the fresh controller knows no values). The bug-1978
* defect cleared only g_needs_full_push -- a daemon that re-pushed onto
* an un-joined HostRole_client endpoint, every push -EPERM forever. */
TEST_ASSERT_EQUAL_INT(1, g_needs_rejoin);
TEST_ASSERT_EQUAL_INT(1, g_needs_full_push);
}
static void test_reconnect_flags_recover_from_any_previous_state(void)
{
/* The handler is level-set, not level-toggle: regardless of what a
* previous (already consumed) reconnect left behind, a new callback
* invocation re-arms both flags. */
g_needs_rejoin = 0;
g_needs_full_push = 1; /* stale leftover from an unconsumed cycle */
TEST_ASSERT_EQUAL_INT(0, esd_on_reconnect((xcecli_ctx_t)0x2, NULL));
TEST_ASSERT_EQUAL_INT(1, g_needs_rejoin);
TEST_ASSERT_EQUAL_INT(1, g_needs_full_push);
}
int main(void)
{
UNITY_BEGIN();
@ -388,6 +427,8 @@ int main(void)
RUN_TEST(test_stale_predicate);
RUN_TEST(test_status_write_basic);
RUN_TEST(test_status_write_marks_stale);
RUN_TEST(test_reconnect_sets_both_rejoin_and_full_push);
RUN_TEST(test_reconnect_flags_recover_from_any_previous_state);
return UNITY_END();
}