feat(libxce-failover): capture redirect hint in handle_reply (before free, before status return); verify endpoint-error wake (task 2.3)
This commit is contained in:
parent
c7e11a4e9e
commit
cec2e647dc
4 changed files with 192 additions and 1 deletions
|
|
@ -29,7 +29,7 @@
|
||||||
- _Requirements: 2.1, 2.2, 2.3, 4.4, 8.4_
|
- _Requirements: 2.1, 2.2, 2.3, 4.4, 8.4_
|
||||||
- _Boundary: EndpointRotator_
|
- _Boundary: EndpointRotator_
|
||||||
- _Depends: 1.1_
|
- _Depends: 1.1_
|
||||||
- [ ] 2.3 (P) Capture redirect on the reply path and wake the wait on endpoint error
|
- [x] 2.3 (P) Capture redirect on the reply path and wake the wait on endpoint error
|
||||||
- In the reply handler, copy the redirect-hint host/port into the client reply context when a redirect reply carries one, before the reply is freed; leave empty otherwise (advisory hint, no sequence correlation)
|
- In the reply handler, copy the redirect-hint host/port into the client reply context when a redirect reply carries one, before the reply is freed; leave empty otherwise (advisory hint, no sequence correlation)
|
||||||
- On endpoint error or peer reset, release any pending reply wait with a transport error so the in-flight call returns instead of hanging
|
- On endpoint error or peer reset, release any pending reply wait with a transport error so the in-flight call returns instead of hanging
|
||||||
- Keep the status flowing through the existing wait-channel path so the public API layer stays unchanged
|
- Keep the status flowing through the existing wait-channel path so the public API layer stays unchanged
|
||||||
|
|
@ -103,3 +103,4 @@
|
||||||
- 1.1: design said `XCE_MAXHOST_LEN`; real constant is `XCE_MAXHOSTNAME_LEN` (config.h, =256) — design corrected. `api_ctx_t` now in private `client_internal.h`; `xcecli_reconnect_cb` typedef lives there until task 4.1 adds the public setter. Build: `cd src && nix develop --command bash tools/xce-build.sh lib`.
|
- 1.1: design said `XCE_MAXHOST_LEN`; real constant is `XCE_MAXHOSTNAME_LEN` (config.h, =256) — design corrected. `api_ctx_t` now in private `client_internal.h`; `xcecli_reconnect_cb` typedef lives there until task 4.1 adds the public setter. Build: `cd src && nix develop --command bash tools/xce-build.sh lib`.
|
||||||
- 1.2 (ADAPTED): RED-against-undefined-symbols would break linking of the shared lib test binary across tasks, so 1.2 builds a GREEN reusable harness (`src/lib/testsuite/failover_harness.h`): fake multi-master config+cursor, an 11-row controller-status taxonomy table (resolves to real errnox.h codes), and an N-pthread runner. Component RED→GREEN tests live in 2.1/2.2/2.3/3.1/3.2. The harness defines `fo_expected_action_t` (FO_ACT_*) mirroring the design `xce_fo_action_t` — 2.1 asserts the real classifier against these rows.
|
- 1.2 (ADAPTED): RED-against-undefined-symbols would break linking of the shared lib test binary across tasks, so 1.2 builds a GREEN reusable harness (`src/lib/testsuite/failover_harness.h`): fake multi-master config+cursor, an 11-row controller-status taxonomy table (resolves to real errnox.h codes), and an N-pthread runner. Component RED→GREEN tests live in 2.1/2.2/2.3/3.1/3.2. The harness defines `fo_expected_action_t` (FO_ACT_*) mirroring the design `xce_fo_action_t` — 2.1 asserts the real classifier against these rows.
|
||||||
- 2.2: EndpointRotator `xce_failover_next_target` (pure, socket-free: hint-first/consume, else cursor advance+wrap over conf->masters[]) + thin `xce_failover_rotate` (close-old/reconnect same endpoint). Live connect path is E2E (6.2). LAYERING: functions taking the private `api_ctx_t` are declared in `src/lib/client_internal.h`, NOT the public `xce/client_failover.h` (which keeps only the pure int-typed classifier) — per src/lib/CLAUDE.md (public headers in src/include/xce/ must not pull private state).
|
- 2.2: EndpointRotator `xce_failover_next_target` (pure, socket-free: hint-first/consume, else cursor advance+wrap over conf->masters[]) + thin `xce_failover_rotate` (close-old/reconnect same endpoint). Live connect path is E2E (6.2). LAYERING: functions taking the private `api_ctx_t` are declared in `src/lib/client_internal.h`, NOT the public `xce/client_failover.h` (which keeps only the pure int-typed classifier) — per src/lib/CLAUDE.md (public headers in src/include/xce/ must not pull private state).
|
||||||
|
- 2.3: RedirectCapture via non-static seam `xce_ctx_capture_redirect_hint()` called in handle_reply BEFORE the capnp free AND before the `status<0` early return (so negative E_XCE_REDIRECT still captures). Bounded strncpy into redirect_hint_host. ReplyWaitWake needed NO production change — existing `handle_disconnect` (err_handler, xcectx.c) already injects -ECONNRESET into wbuf + sets XCE_CTX_ERR; full err_handler path is covered by E2E (6.2).
|
||||||
|
|
|
||||||
|
|
@ -59,4 +59,19 @@ extern int xce_ctx_set_endpoint(xce_ctx_t*, endpoint_t*);
|
||||||
extern void xce_block_tasks(xce_ctx_t*);
|
extern void xce_block_tasks(xce_ctx_t*);
|
||||||
extern void xce_unblock_tasks(xce_ctx_t*);
|
extern void xce_unblock_tasks(xce_ctx_t*);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* xce_ctx_capture_redirect_hint: copy a redirect address carried on a reply
|
||||||
|
* into the ctx's redirect_hint_host/port fields as a best-effort failover hint
|
||||||
|
* (Req 2.2, 8.3). Called by handle_reply() BEFORE the capnp-owned reply (and
|
||||||
|
* its redirect_host string) is freed.
|
||||||
|
*
|
||||||
|
* When `host` is NULL/empty or `port` is 0 there is no usable address: the hint
|
||||||
|
* is left untouched (it stays empty). Otherwise the host is bounded-copied into
|
||||||
|
* the fixed-size field (truncated + NUL-terminated, never overflowing) and the
|
||||||
|
* port is stored. A NULL ctx is a safe no-op. Advisory only — correctness never
|
||||||
|
* depends on the hint.
|
||||||
|
*/
|
||||||
|
extern void xce_ctx_capture_redirect_hint(xce_ctx_t*, const char* host,
|
||||||
|
uint16_t port);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -617,6 +617,150 @@ static void test_next_target_rejects_bad_args(void)
|
||||||
printf("test_next_target_rejects_bad_args: PASS\n");
|
printf("test_next_target_rejects_bad_args: PASS\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════════════════════
|
||||||
|
* Task 2.3 — RedirectCapture (xce_ctx_capture_redirect_hint) RED→GREEN tests.
|
||||||
|
*
|
||||||
|
* handle_reply() copies the redirect address carried on a redirect reply into
|
||||||
|
* the ctx's redirect_hint_host/port BEFORE the capnp-owned reply is freed. The
|
||||||
|
* copy logic is factored into a non-static seam, xce_ctx_capture_redirect_hint,
|
||||||
|
* so it can be unit-tested without a live server.
|
||||||
|
*
|
||||||
|
* Requirements: 2.2 (capture redirect addr as a best-effort hint), 8.3 (try the
|
||||||
|
* hinted address first — capture is the prerequisite).
|
||||||
|
* ════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* Build a bare xce_ctx_t with an empty redirect hint, without touching any
|
||||||
|
* iomgr/ssl/endpoint state (capture is pure ctx-field manipulation). */
|
||||||
|
static void fo_ctx_clear_hint(xce_ctx_t *ctx)
|
||||||
|
{
|
||||||
|
memset(ctx, 0, sizeof(*ctx));
|
||||||
|
ctx->redirect_hint_host[0] = '\0';
|
||||||
|
ctx->redirect_hint_port = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Req 2.2, 8.3: a present host+port is copied into the ctx hint fields. */
|
||||||
|
static void test_capture_redirect_hint_present(void)
|
||||||
|
{
|
||||||
|
xce_ctx_t ctx;
|
||||||
|
fo_ctx_clear_hint(&ctx);
|
||||||
|
|
||||||
|
xce_ctx_capture_redirect_hint(&ctx, "10.40.0.5", 9300);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("10.40.0.5", ctx.redirect_hint_host);
|
||||||
|
TEST_ASSERT_EQUAL_INT(9300, ctx.redirect_hint_port);
|
||||||
|
|
||||||
|
printf("test_capture_redirect_hint_present: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Req 2.2: when no address is present (host NULL/empty or port 0) the hint is
|
||||||
|
* left empty — capture must not invent an address. */
|
||||||
|
static void test_capture_redirect_hint_absent_stays_empty(void)
|
||||||
|
{
|
||||||
|
xce_ctx_t ctx;
|
||||||
|
|
||||||
|
/* NULL host → hint untouched */
|
||||||
|
fo_ctx_clear_hint(&ctx);
|
||||||
|
xce_ctx_capture_redirect_hint(&ctx, NULL, 9300);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", ctx.redirect_hint_host);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, ctx.redirect_hint_port);
|
||||||
|
|
||||||
|
/* empty host → hint untouched */
|
||||||
|
fo_ctx_clear_hint(&ctx);
|
||||||
|
xce_ctx_capture_redirect_hint(&ctx, "", 9300);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", ctx.redirect_hint_host);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, ctx.redirect_hint_port);
|
||||||
|
|
||||||
|
/* port 0 → hint untouched (no usable target) */
|
||||||
|
fo_ctx_clear_hint(&ctx);
|
||||||
|
xce_ctx_capture_redirect_hint(&ctx, "10.40.0.5", 0);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", ctx.redirect_hint_host);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, ctx.redirect_hint_port);
|
||||||
|
|
||||||
|
printf("test_capture_redirect_hint_absent_stays_empty: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Req 2.2: an over-long host is bounded-copied (NUL-terminated, no overflow);
|
||||||
|
* the capnp-owned string the controller may send is not length-checked here. */
|
||||||
|
static void test_capture_redirect_hint_truncates_long_host(void)
|
||||||
|
{
|
||||||
|
xce_ctx_t ctx;
|
||||||
|
fo_ctx_clear_hint(&ctx);
|
||||||
|
|
||||||
|
char longhost[XCE_MAXHOSTNAME_LEN * 2];
|
||||||
|
memset(longhost, 'h', sizeof(longhost) - 1);
|
||||||
|
longhost[sizeof(longhost) - 1] = '\0';
|
||||||
|
|
||||||
|
xce_ctx_capture_redirect_hint(&ctx, longhost, 9400);
|
||||||
|
|
||||||
|
/* copied, NUL-terminated, and never longer than the field bound */
|
||||||
|
TEST_ASSERT_EQUAL_INT(XCE_MAXHOSTNAME_LEN - 1,
|
||||||
|
(int)strlen(ctx.redirect_hint_host));
|
||||||
|
TEST_ASSERT_EQUAL_INT('\0',
|
||||||
|
ctx.redirect_hint_host[XCE_MAXHOSTNAME_LEN - 1]);
|
||||||
|
TEST_ASSERT_EQUAL_INT(9400, ctx.redirect_hint_port);
|
||||||
|
|
||||||
|
printf("test_capture_redirect_hint_truncates_long_host: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Guard: a NULL ctx is rejected without crashing. */
|
||||||
|
static void test_capture_redirect_hint_null_ctx_safe(void)
|
||||||
|
{
|
||||||
|
/* must not crash */
|
||||||
|
xce_ctx_capture_redirect_hint(NULL, "10.40.0.5", 9300);
|
||||||
|
printf("test_capture_redirect_hint_null_ctx_safe: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════════════════════
|
||||||
|
* Task 2.3 — ReplyWaitWake RED→GREEN test.
|
||||||
|
*
|
||||||
|
* On an endpoint error/peer reset, the existing disconnect path injects a
|
||||||
|
* transport error into ctx->wbuf so a blocked cbuf_get returns instead of
|
||||||
|
* hanging. This test proves the wake mechanism end-to-end on a real wbuf: a
|
||||||
|
* thread blocks in cbuf_get, the "disconnect" injects -ECONNRESET, and the
|
||||||
|
* blocked waiter wakes with exactly that transport error (Req 4.4).
|
||||||
|
* ════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
cbuf_t wbuf;
|
||||||
|
int woke_status; /* what the blocked cbuf_get observed */
|
||||||
|
int got; /* cbuf_get return code */
|
||||||
|
} fo_wake_ctx_t;
|
||||||
|
|
||||||
|
static void fo_wake_waiter(void *p, int idx)
|
||||||
|
{
|
||||||
|
fo_wake_ctx_t *w = (fo_wake_ctx_t *)p;
|
||||||
|
if (idx == 0) {
|
||||||
|
/* waiter: block until the disconnect injects an error */
|
||||||
|
w->got = cbuf_get(&w->wbuf, &w->woke_status);
|
||||||
|
} else {
|
||||||
|
/* injector: mirror handle_disconnect's wbuf error injection. A short
|
||||||
|
* spin lets the waiter reach cbuf_get first so we exercise the wake,
|
||||||
|
* not a pre-filled buffer. */
|
||||||
|
for (volatile int i = 0; i < 2000000; i++) {
|
||||||
|
}
|
||||||
|
int err = -ECONNRESET;
|
||||||
|
cbuf_tryput(&w->wbuf, &err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Req 4.4: a pending cbuf_get is released with the injected transport error so
|
||||||
|
* an in-flight xce_* call returns instead of hanging across an endpoint reset. */
|
||||||
|
static void test_reply_wait_wake_releases_blocked_get(void)
|
||||||
|
{
|
||||||
|
fo_wake_ctx_t w;
|
||||||
|
memset(&w, 0, sizeof(w));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, cbuf_init(&w.wbuf, 1, sizeof(int)));
|
||||||
|
|
||||||
|
int rc = fo_run_threads(2, fo_wake_waiter, &w);
|
||||||
|
cbuf_fini(&w.wbuf);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||||
|
TEST_ASSERT_TRUE(w.got >= 0); /* cbuf_get returned, did not hang */
|
||||||
|
TEST_ASSERT_EQUAL_INT(-ECONNRESET, w.woke_status);
|
||||||
|
|
||||||
|
printf("test_reply_wait_wake_releases_blocked_get: PASS\n");
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Suite entry point ──────────────────────────────────────────────── */
|
/* ── Suite entry point ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
void test_libxce_retry(void)
|
void test_libxce_retry(void)
|
||||||
|
|
@ -638,5 +782,10 @@ void test_libxce_retry(void)
|
||||||
RUN_TEST(test_next_target_rotates_and_wraps);
|
RUN_TEST(test_next_target_rotates_and_wraps);
|
||||||
RUN_TEST(test_next_target_hint_then_fallback_rotation);
|
RUN_TEST(test_next_target_hint_then_fallback_rotation);
|
||||||
RUN_TEST(test_next_target_rejects_bad_args);
|
RUN_TEST(test_next_target_rejects_bad_args);
|
||||||
|
RUN_TEST(test_capture_redirect_hint_present);
|
||||||
|
RUN_TEST(test_capture_redirect_hint_absent_stays_empty);
|
||||||
|
RUN_TEST(test_capture_redirect_hint_truncates_long_host);
|
||||||
|
RUN_TEST(test_capture_redirect_hint_null_ctx_safe);
|
||||||
|
RUN_TEST(test_reply_wait_wake_releases_blocked_get);
|
||||||
UNITY_END();
|
UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,36 @@ static int handle_sub_reply(xce_ctx_t* ctx, xce_reply_t* reply)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void xce_ctx_capture_redirect_hint(xce_ctx_t* ctx, const char* host,
|
||||||
|
uint16_t port)
|
||||||
|
{
|
||||||
|
if (ctx == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No usable redirect address → leave the hint untouched (empty). The
|
||||||
|
* capnp-owned host pointer may be NULL; an empty host or a zero port is
|
||||||
|
* likewise "no address". Advisory only — correctness never depends on it. */
|
||||||
|
if ((host == NULL) || (host[0] == '\0') || (port == 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
strncpy(ctx->redirect_hint_host, host, XCE_MAXHOSTNAME_LEN - 1);
|
||||||
|
ctx->redirect_hint_host[XCE_MAXHOSTNAME_LEN - 1] = '\0';
|
||||||
|
ctx->redirect_hint_port = port;
|
||||||
|
}
|
||||||
|
|
||||||
static int handle_reply(xce_ctx_t* ctx, xce_reply_t* reply)
|
static int handle_reply(xce_ctx_t* ctx, xce_reply_t* reply)
|
||||||
{
|
{
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
|
|
||||||
|
/* Capture the redirect address (if any) as a best-effort failover hint
|
||||||
|
* BEFORE the capnp-owned reply — including reply->redirect_host — is freed
|
||||||
|
* by the caller (Req 2.2, 8.3). The status still flows through the existing
|
||||||
|
* cbuf_tryput(&ctx->wbuf,&status) path unchanged, so xceapi.c is untouched. */
|
||||||
|
xce_ctx_capture_redirect_hint(ctx, reply->redirect_host,
|
||||||
|
reply->redirect_port);
|
||||||
|
|
||||||
if (reply->status < 0) {
|
if (reply->status < 0) {
|
||||||
return reply->status;
|
return reply->status;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue