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
|
|
@ -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_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
|
||||
|
|
|
|||
|
|
@ -617,6 +617,150 @@ static void test_next_target_rejects_bad_args(void)
|
|||
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 ──────────────────────────────────────────────── */
|
||||
|
||||
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_hint_then_fallback_rotation);
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,10 +27,36 @@ static int handle_sub_reply(xce_ctx_t* ctx, xce_reply_t* reply)
|
|||
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)
|
||||
{
|
||||
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) {
|
||||
return reply->status;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue