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:
parent
b38ef6de63
commit
b471ce31ca
5 changed files with 219 additions and 7 deletions
|
|
@ -23,10 +23,10 @@ xceesd = executable('xceesd',
|
|||
dependencies: esd_deps,
|
||||
install: true)
|
||||
|
||||
# Unit tests: white-box #include of the pure-core sources (config parse,
|
||||
# script-output parse) -- same convention as the worker's
|
||||
# resource_sync_test. The join/reconnect wiring (esd.c) drives the live
|
||||
# client library and is covered by the t1 E2E, not unit tests.
|
||||
# Unit tests: white-box #include of the core sources (config parse,
|
||||
# script-output parse, status writer, and -- since bug-1978 -- esd.c's
|
||||
# reconnect-flag contract; esd_run's live-join sequence is still covered by
|
||||
# the t1 E2E) -- same convention as the worker's resource_sync_test.
|
||||
esd_test_incdir = include_directories('include', '../include', '../lib', '.')
|
||||
esd_test_deps = [libxce_dep, yaml_deps, tinylog_dep, unity_dep]
|
||||
|
||||
|
|
|
|||
|
|
@ -159,14 +159,24 @@ int esd_push_all(void)
|
|||
/* --rejoin hook (client library contract): the library calls this while
|
||||
* holding its failover lock -- no I/O, no xcecli_* calls (would self-
|
||||
* deadlock, the worker's xcecd_on_reconnect lesson, controller_connector.c).
|
||||
* We only set a flag; the main loop consumes it after the lock is free. */
|
||||
* We only set flags; the main loop consumes them after the lock is free. */
|
||||
static volatile int g_needs_full_push = 0;
|
||||
static volatile int g_needs_rejoin = 0;
|
||||
|
||||
static int esd_on_reconnect(xcecli_ctx_t ctx, void* arg)
|
||||
{
|
||||
(void)ctx;
|
||||
(void)arg;
|
||||
|
||||
/* bug-1978: a re-PUSH alone is not enough -- the fresh controller
|
||||
* endpoint was created by handle_connection() with priv->role =
|
||||
* HostRole_client, and only OPCode_join sets it to HostRole_extres
|
||||
* (join.c). Without the re-join every resval push lands in
|
||||
* do_update_resvals's neither-branch and is refused -EPERM forever
|
||||
* (the live sighting: pushes_ok frozen, rc=-1 on every attempt after
|
||||
* a controller restart). Mirror the worker's needs_rejoin contract:
|
||||
* the main loop re-issues the role join, then does the full re-push. */
|
||||
g_needs_rejoin = 1;
|
||||
g_needs_full_push = 1;
|
||||
|
||||
return 0;
|
||||
|
|
@ -250,6 +260,28 @@ int esd_run(void)
|
|||
while (g_esd.running) {
|
||||
int i;
|
||||
|
||||
if (g_needs_rejoin) {
|
||||
/* bug-1978: a transparent reconnect just landed -- the fresh
|
||||
* controller endpoint defaults to HostRole_client, so the role
|
||||
* join must be re-issued BEFORE any push (an un-joined push is
|
||||
* refused -EPERM). This runs on the main loop, outside the
|
||||
* library's failover lock (the xcecd_on_reconnect constraint).
|
||||
* Failure keeps the flag set: the next tick retries, mirroring
|
||||
* the worker's needs_rejoin self-healing cadence. rc >= 0 is
|
||||
* success for the same reason as the worker's rejoin (the
|
||||
* controller's reply.status for a plain extres join is 0, but
|
||||
* the failover wrapper surfaces its own codes on success). */
|
||||
int rc = xcecli_join(g_esd.apictx, HostRole_extres, NULL);
|
||||
if (rc >= 0) {
|
||||
g_needs_rejoin = 0;
|
||||
INFO("esd: re-joined after reconnect (rc=%d)", rc);
|
||||
} else {
|
||||
WARN("esd: re-join after reconnect failed (rc=%d) -- "
|
||||
"retrying next tick",
|
||||
rc);
|
||||
}
|
||||
}
|
||||
|
||||
if (g_needs_full_push) {
|
||||
/* A transparent reconnect just landed (new primary after
|
||||
* failover, or the old controller restarted): it has no
|
||||
|
|
@ -272,8 +304,10 @@ int esd_run(void)
|
|||
* The broken-connection flag is set asynchronously by the
|
||||
* transport's RDHUP handler (bug-1760's pure predicate): when it
|
||||
* latches, force one full re-push -- the write attempt triggers the
|
||||
* rotate/reconnect, and esd_on_reconnect's flag keeps the full
|
||||
* re-push contract for the failover path too. */
|
||||
* rotate/reconnect, and esd_on_reconnect's flags keep the
|
||||
* re-join + full re-push contract for the failover path too
|
||||
* (bug-1978: the re-push alone was refused -EPERM on the un-joined
|
||||
* fresh endpoint). */
|
||||
if (g_esd.cfg.nsrc > 0 && xcecli_is_connection_broken(g_esd.apictx)) {
|
||||
INFO("esd: connection broken -- forcing a full re-push to "
|
||||
"reconnect");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue