feat(libxce-failover): 4-action status classifier with attempt-aware DUP-replay rule (task 2.1)
This commit is contained in:
parent
d11c6e1d89
commit
fa2ac05036
5 changed files with 222 additions and 1 deletions
|
|
@ -15,7 +15,7 @@
|
|||
- _Requirements: 2.5, 4.1, 4.2, 4.3, 4.4, 4.5, 5.2, 8.3, 8.4_
|
||||
|
||||
- [ ] 2. Core: status classification, endpoint rotation, reply-path capture
|
||||
- [ ] 2.1 Implement status classification
|
||||
- [x] 2.1 Implement status classification
|
||||
- Map each controller status and transport result to one of four actions: success, retry-now (redirect/standby/transport — rotate without backoff), retry-after-backoff (no-primary/not-ready), terminal
|
||||
- Never classify redirect, standby, no-primary, or not-ready as success
|
||||
- Treat a duplicate-job/exists status on a re-sent write as success (idempotent replay); on a first attempt treat it as a genuine duplicate surfaced to the caller
|
||||
|
|
|
|||
49
src/include/xce/client_failover.h
Normal file
49
src/include/xce/client_failover.h
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* client_failover.h — public StatusClassifier surface for the libxce-failover
|
||||
* spec.
|
||||
*
|
||||
* Task 2.1 (StatusClassifier): pure mapping from a reply status / transport rc
|
||||
* to a failover action. No I/O, no globals — see design.md
|
||||
* "Components and Interfaces → StatusClassifier".
|
||||
*/
|
||||
#ifndef XCE_CLIENT_FAILOVER_H
|
||||
#define XCE_CLIENT_FAILOVER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Failover action a round-trip outcome maps to. Order mirrors the harness'
|
||||
* fo_expected_action_t (failover_harness.h) and design.md. */
|
||||
typedef enum {
|
||||
XCE_FO_SUCCESS, /* status == 0, or idempotent-replay success
|
||||
(DUP on a re-sent write) */
|
||||
XCE_FO_RETRY_NOW, /* E_XCE_REDIRECT / E_XCE_STANDBY / transport error
|
||||
— rotate, no backoff */
|
||||
XCE_FO_RETRY_BACKOFF, /* E_XCE_NOMASTER / E_XCE_NOT_READY — rotate after
|
||||
jittered backoff */
|
||||
XCE_FO_TERMINAL /* AUTH/PERM/INVALID/NOENT and other non-retriable
|
||||
— surface to caller */
|
||||
} xce_fo_action_t;
|
||||
|
||||
/*
|
||||
* Map a reply status / transport rc to a failover action.
|
||||
*
|
||||
* status : the controller reply status or negative -errno transport rc, as
|
||||
* the failover loop observes it. Controller codes (E_XCE_*) are
|
||||
* accepted in either sign.
|
||||
* is_write: true for mutating ops (e.g. job submit); false for reads.
|
||||
* attempt : 0 for the first send; >0 for a re-send after failover.
|
||||
*
|
||||
* Invariant (2.5): E_XCE_REDIRECT/STANDBY/NOMASTER/NOT_READY never map to
|
||||
* XCE_FO_SUCCESS.
|
||||
*
|
||||
* Idempotent-replay rule (5.2): E_XCE_DUP_JOB / EEXIST on a re-sent write
|
||||
* (is_write && attempt > 0) → XCE_FO_SUCCESS; otherwise → XCE_FO_TERMINAL.
|
||||
*
|
||||
* Unknown / unrecognized statuses are treated conservatively as
|
||||
* XCE_FO_TERMINAL (surfaced, not retried).
|
||||
*
|
||||
* Pure function: no I/O, no globals.
|
||||
*/
|
||||
xce_fo_action_t xce_failover_classify(int status, bool is_write, int attempt);
|
||||
|
||||
#endif /* XCE_CLIENT_FAILOVER_H */
|
||||
60
src/lib/client_failover.c
Normal file
60
src/lib/client_failover.c
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* client_failover.c — StatusClassifier for the libxce-failover spec (task 2.1).
|
||||
*
|
||||
* Pure mapping from a reply status / transport rc to a failover action. No I/O,
|
||||
* no globals. See design.md "Components and Interfaces → StatusClassifier" and
|
||||
* requirements 2.5, 4.1, 4.2, 4.3, 4.5, 5.2.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "xce/client_failover.h"
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
#include "xce/errnox.h"
|
||||
|
||||
/*
|
||||
* Controller status codes (E_XCE_*) are positive enum values, but the failover
|
||||
* loop may observe them as negative -errno-style rc. Accept either sign by
|
||||
* comparing against the magnitude.
|
||||
*/
|
||||
static int fo_abs(int v)
|
||||
{
|
||||
return v < 0 ? -v : v;
|
||||
}
|
||||
|
||||
xce_fo_action_t xce_failover_classify(int status, bool is_write, int attempt)
|
||||
{
|
||||
/* Success: served by the primary. */
|
||||
if (status == 0)
|
||||
return XCE_FO_SUCCESS;
|
||||
|
||||
int code = fo_abs(status);
|
||||
|
||||
/* Idempotent-replay rule (5.2): a duplicate on a re-sent write means the
|
||||
* controller's client-jobid replay guard already applied our write — treat
|
||||
* it as success. On the first send (or a non-write), it is a genuine
|
||||
* duplicate and is surfaced unchanged. */
|
||||
if (code == E_XCE_DUP_JOB || code == EEXIST) {
|
||||
if (is_write && attempt > 0)
|
||||
return XCE_FO_SUCCESS;
|
||||
return XCE_FO_TERMINAL;
|
||||
}
|
||||
|
||||
/* Redirect / standby: a primary exists elsewhere, or this op is
|
||||
* primary-only — rotate promptly, no backoff (4.2, 4.3). */
|
||||
if (code == E_XCE_REDIRECT || code == E_XCE_STANDBY)
|
||||
return XCE_FO_RETRY_NOW;
|
||||
|
||||
/* No primary known yet / controller initializing or electing — rotate after
|
||||
* jittered backoff (4.1). */
|
||||
if (code == E_XCE_NOMASTER || code == E_XCE_NOT_READY)
|
||||
return XCE_FO_RETRY_BACKOFF;
|
||||
|
||||
/* Transport-level failures — reconnect/rotate and retry now (4.4). */
|
||||
if (code == ECONNREFUSED || code == ETIMEDOUT || code == ECONNRESET)
|
||||
return XCE_FO_RETRY_NOW;
|
||||
|
||||
/* Everything else — auth/permission/invalid/not-found and any unrecognized
|
||||
* status — is non-retriable and surfaced to the caller (4.5). */
|
||||
return XCE_FO_TERMINAL;
|
||||
}
|
||||
|
|
@ -68,6 +68,7 @@ libxce_src = [ gen_srcs[0],
|
|||
xce_resreq_parser[0],
|
||||
xce_select_parser[0],
|
||||
'client.c',
|
||||
'client_failover.c',
|
||||
'dataq.c',
|
||||
'hook.c',
|
||||
'htab.c',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@
|
|||
|
||||
#include "failover_harness.h"
|
||||
#include "request.h"
|
||||
#include "xce/client_failover.h"
|
||||
#include "xce/errnox.h"
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
* Harness implementation
|
||||
|
|
@ -344,6 +346,110 @@ static void test_thread_harness_rejects_bad_args(void)
|
|||
printf("test_thread_harness_rejects_bad_args: PASS\n");
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
* Task 2.1 — StatusClassifier (xce_failover_classify) RED→GREEN tests
|
||||
* ════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Map the harness' expected-action category to the real classifier enum. The
|
||||
* two enums share order by design, but we map explicitly so a future reorder of
|
||||
* either enum is caught here rather than silently passing. */
|
||||
static xce_fo_action_t fo_expected_to_action(fo_expected_action_t e)
|
||||
{
|
||||
switch (e) {
|
||||
case FO_ACT_SUCCESS: return XCE_FO_SUCCESS;
|
||||
case FO_ACT_RETRY_NOW: return XCE_FO_RETRY_NOW;
|
||||
case FO_ACT_RETRY_BACKOFF: return XCE_FO_RETRY_BACKOFF;
|
||||
case FO_ACT_TERMINAL: return XCE_FO_TERMINAL;
|
||||
}
|
||||
return XCE_FO_TERMINAL;
|
||||
}
|
||||
|
||||
/* Req 2.5, 4.1, 4.2, 4.3, 4.5: every taxonomy row's first-send classification
|
||||
* (is_write=false, attempt=0) matches the design's StatusClassifier mapping. */
|
||||
static void test_classify_matches_taxonomy_table(void)
|
||||
{
|
||||
for (size_t i = 0; i < FO_STATUS_TABLE_LEN; i++) {
|
||||
const fo_status_row_t *row = &FO_STATUS_TABLE[i];
|
||||
xce_fo_action_t want = fo_expected_to_action(row->expect);
|
||||
xce_fo_action_t got =
|
||||
xce_failover_classify(row->status, /*is_write=*/false,
|
||||
/*attempt=*/0);
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), "row '%s' (status=%d): want %d got %d",
|
||||
row->name, row->status, (int)want, (int)got);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(want, got, msg);
|
||||
}
|
||||
|
||||
printf("test_classify_matches_taxonomy_table: PASS\n");
|
||||
}
|
||||
|
||||
/* Req 2.5: REDIRECT/STANDBY/NOMASTER/NOT_READY are never reported as success,
|
||||
* regardless of is_write/attempt. */
|
||||
static void test_classify_control_codes_never_success(void)
|
||||
{
|
||||
const int codes[] = { E_XCE_REDIRECT, E_XCE_STANDBY, E_XCE_NOMASTER,
|
||||
E_XCE_NOT_READY };
|
||||
for (size_t i = 0; i < sizeof(codes) / sizeof(codes[0]); i++) {
|
||||
TEST_ASSERT_NOT_EQUAL(XCE_FO_SUCCESS,
|
||||
xce_failover_classify(codes[i], false, 0));
|
||||
TEST_ASSERT_NOT_EQUAL(XCE_FO_SUCCESS,
|
||||
xce_failover_classify(codes[i], true, 1));
|
||||
/* accepted in either sign */
|
||||
TEST_ASSERT_NOT_EQUAL(XCE_FO_SUCCESS,
|
||||
xce_failover_classify(-codes[i], true, 1));
|
||||
}
|
||||
|
||||
printf("test_classify_control_codes_never_success: PASS\n");
|
||||
}
|
||||
|
||||
/* Req 5.2: idempotent-replay rule — DUP_JOB/EEXIST on a RE-SENT WRITE is a
|
||||
* success; the first send (or a read) is a genuine duplicate → terminal. */
|
||||
static void test_classify_dup_replay_rule(void)
|
||||
{
|
||||
/* re-sent write (is_write && attempt>0) → SUCCESS */
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_SUCCESS,
|
||||
xce_failover_classify(-E_XCE_DUP_JOB, true, 1));
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_SUCCESS,
|
||||
xce_failover_classify(E_XCE_DUP_JOB, true, 1));
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_SUCCESS,
|
||||
xce_failover_classify(-EEXIST, true, 1));
|
||||
|
||||
/* first send → genuine duplicate → TERMINAL */
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL,
|
||||
xce_failover_classify(-E_XCE_DUP_JOB, true, 0));
|
||||
/* non-write replay → TERMINAL */
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL,
|
||||
xce_failover_classify(-E_XCE_DUP_JOB, false, 1));
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL,
|
||||
xce_failover_classify(-EEXIST, false, 1));
|
||||
|
||||
printf("test_classify_dup_replay_rule: PASS\n");
|
||||
}
|
||||
|
||||
/* Req 3.2 / general: status 0 is always success. */
|
||||
static void test_classify_zero_is_success(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_SUCCESS,
|
||||
xce_failover_classify(0, false, 0));
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_SUCCESS,
|
||||
xce_failover_classify(0, true, 3));
|
||||
|
||||
printf("test_classify_zero_is_success: PASS\n");
|
||||
}
|
||||
|
||||
/* Req 4.5: unknown / unrecognized statuses are conservatively terminal. */
|
||||
static void test_classify_unknown_is_terminal(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL,
|
||||
xce_failover_classify(-EACCES, false, 0));
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL,
|
||||
xce_failover_classify(-ENOENT, true, 1));
|
||||
TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL,
|
||||
xce_failover_classify(-12345, false, 0));
|
||||
|
||||
printf("test_classify_unknown_is_terminal: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Suite entry point ──────────────────────────────────────────────── */
|
||||
|
||||
void test_libxce_retry(void)
|
||||
|
|
@ -356,5 +462,10 @@ void test_libxce_retry(void)
|
|||
RUN_TEST(test_status_table_redirect_hint_flag);
|
||||
RUN_TEST(test_thread_harness_runs_all_threads);
|
||||
RUN_TEST(test_thread_harness_rejects_bad_args);
|
||||
RUN_TEST(test_classify_matches_taxonomy_table);
|
||||
RUN_TEST(test_classify_control_codes_never_success);
|
||||
RUN_TEST(test_classify_dup_replay_rule);
|
||||
RUN_TEST(test_classify_zero_is_success);
|
||||
RUN_TEST(test_classify_unknown_is_terminal);
|
||||
UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue