feat(libxce-failover): reconnect/rejoin hook — public setter + post-reconnect invocation before resume (task 4.1)

This commit is contained in:
Rongsong Shen 2026-06-17 03:46:05 +00:00
parent 12284bc3f5
commit 8783ce5a9d
6 changed files with 264 additions and 26 deletions

View file

@ -1127,6 +1127,151 @@ static void test_recover_null_ctx_safe(void)
printf("test_recover_null_ctx_safe: PASS\n");
}
/* ════════════════════════════════════════════════════════════════════════
* Task 4.1 — ReconnectRejoinHook (xcecli_set_reconnect_handler +
* xcecli_invoke_rejoin) RED→GREEN tests.
*
* libxce owns the hook MECHANISM + ORDERING (reconnect → hook → resume); the
* callback CONTENT is the consumer's. The failover machinery invokes the
* registered callback once per successful reconnect, AFTER reconnect/recovery
* and BEFORE the interrupted op resumes. A hook returning <0 means the new
* primary rejected the daemon's join → treat it as a FAILED attempt (do not
* resume; the loop rotates/backs off again). Pure clients register NO hook
* (NULL) → invoke is a no-op returning 0 and the loop resumes directly.
*
* xcecli_invoke_rejoin is socket-free (it only calls the stored cb), so it is
* unit-testable directly here without a live server.
*
* Requirements: 1.2 (re-establish session against new primary before resume).
* ════════════════════════════════════════════════════════════════════════ */
/* A test rejoin callback: records that it ran (and the ctx/arg it saw) and
* returns a caller-controlled rc so we can drive the success and failed-join
* paths. */
typedef struct {
int called; /* number of times the hook fired */
xcecli_ctx_t seen_ctx; /* the ctx the hook observed */
void *seen_arg; /* the opaque arg the hook observed */
int ret; /* rc the hook returns (0 ok, <0 failed rejoin) */
} fo_rejoin_probe_t;
static int fo_rejoin_cb(xcecli_ctx_t ctx, void *arg)
{
fo_rejoin_probe_t *p = (fo_rejoin_probe_t *)arg;
p->called++;
p->seen_ctx = ctx;
p->seen_arg = arg;
return p->ret;
}
/* Req 1.2: the setter stores cb+arg into the failover state; invoke then calls
* the registered cb exactly once, passing the opaque ctx and arg, and returns
* the cb's rc (0 on a successful rejoin). */
static void test_invoke_rejoin_calls_registered_hook(void)
{
xce_client_config_t conf;
api_ctx_t ac;
fo_make_client_masters(&conf);
fo_make_api_ctx(&ac, &conf);
fo_rejoin_probe_t probe = { 0 };
probe.ret = 0;
/* setter stores cb+arg on the failover state */
int src = xcecli_set_reconnect_handler((xcecli_ctx_t)&ac, fo_rejoin_cb,
&probe);
TEST_ASSERT_EQUAL_INT(0, src);
TEST_ASSERT_TRUE(ac.failover.rejoin_cb == fo_rejoin_cb);
TEST_ASSERT_TRUE(ac.failover.rejoin_arg == &probe);
/* invoke fires the hook once, returns its rc (0), passes ctx + arg */
int rc = xcecli_invoke_rejoin(&ac);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(1, probe.called);
TEST_ASSERT_TRUE(probe.seen_ctx == (xcecli_ctx_t)&ac);
TEST_ASSERT_TRUE(probe.seen_arg == &probe);
printf("test_invoke_rejoin_calls_registered_hook: PASS\n");
}
/* Req 1.2: a hook that returns <0 (the new primary rejected the join) makes
* invoke return that <0 — the wrapper treats it as a failed attempt and does
* NOT resume the interrupted op. */
static void test_invoke_rejoin_propagates_failure(void)
{
xce_client_config_t conf;
api_ctx_t ac;
fo_make_client_masters(&conf);
fo_make_api_ctx(&ac, &conf);
fo_rejoin_probe_t probe = { 0 };
probe.ret = -ECONNREFUSED; /* join rejected by new primary */
TEST_ASSERT_EQUAL_INT(
0, xcecli_set_reconnect_handler((xcecli_ctx_t)&ac, fo_rejoin_cb,
&probe));
int rc = xcecli_invoke_rejoin(&ac);
TEST_ASSERT_EQUAL_INT(-ECONNREFUSED, rc); /* failure surfaced to the loop */
TEST_ASSERT_EQUAL_INT(1, probe.called);
printf("test_invoke_rejoin_propagates_failure: PASS\n");
}
/* Req 1.2: a pure client registers NO hook (NULL) → invoke is a no-op that
* returns 0 and calls nothing, so the loop resumes directly. */
static void test_invoke_rejoin_no_hook_is_noop(void)
{
xce_client_config_t conf;
api_ctx_t ac;
fo_make_client_masters(&conf);
fo_make_api_ctx(&ac, &conf); /* leaves rejoin_cb == NULL */
TEST_ASSERT_NULL(ac.failover.rejoin_cb);
int rc = xcecli_invoke_rejoin(&ac);
TEST_ASSERT_EQUAL_INT(0, rc); /* no hook → no-op success */
printf("test_invoke_rejoin_no_hook_is_noop: PASS\n");
}
/* Req 1.2: the setter is NULL-safe on a NULL ctx, and can clear a previously
* registered hook by passing NULL (back to the pure-client no-op path). */
static void test_set_reconnect_handler_null_safe_and_clears(void)
{
xce_client_config_t conf;
api_ctx_t ac;
fo_make_client_masters(&conf);
fo_make_api_ctx(&ac, &conf);
/* NULL ctx rejected without crashing */
TEST_ASSERT_EQUAL_INT(-EINVAL,
xcecli_set_reconnect_handler(NULL, fo_rejoin_cb,
NULL));
fo_rejoin_probe_t probe = { 0 };
TEST_ASSERT_EQUAL_INT(
0, xcecli_set_reconnect_handler((xcecli_ctx_t)&ac, fo_rejoin_cb,
&probe));
TEST_ASSERT_TRUE(ac.failover.rejoin_cb == fo_rejoin_cb);
/* clearing with a NULL cb restores the no-hook no-op path */
TEST_ASSERT_EQUAL_INT(
0, xcecli_set_reconnect_handler((xcecli_ctx_t)&ac, NULL, NULL));
TEST_ASSERT_NULL(ac.failover.rejoin_cb);
TEST_ASSERT_EQUAL_INT(0, xcecli_invoke_rejoin(&ac));
TEST_ASSERT_EQUAL_INT(0, probe.called); /* cleared hook never fired */
printf("test_set_reconnect_handler_null_safe_and_clears: PASS\n");
}
/* Guard: invoke on a NULL api_ctx is rejected without crashing. */
static void test_invoke_rejoin_null_ctx_safe(void)
{
TEST_ASSERT_EQUAL_INT(-EINVAL, xcecli_invoke_rejoin(NULL));
printf("test_invoke_rejoin_null_ctx_safe: PASS\n");
}
/* ── Suite entry point ──────────────────────────────────────────────── */
void test_libxce_retry(void)
@ -1165,5 +1310,10 @@ void test_libxce_retry(void)
RUN_TEST(test_recover_leaves_down_state_untouched);
RUN_TEST(test_recover_drains_stale_wbuf_entry);
RUN_TEST(test_recover_null_ctx_safe);
RUN_TEST(test_invoke_rejoin_calls_registered_hook);
RUN_TEST(test_invoke_rejoin_propagates_failure);
RUN_TEST(test_invoke_rejoin_no_hook_is_noop);
RUN_TEST(test_set_reconnect_handler_null_safe_and_clears);
RUN_TEST(test_invoke_rejoin_null_ctx_safe);
UNITY_END();
}