test(ha-commit-barrier-rollout): RED tests for classification totality (task 1.1)
New test_commit_barrier_rollout.c: per-type rows matching the VERIFIED design table (STOP/RESUME/UPDATE/FAIL RETAIN per supersession), 13-value totality sweep, fork/SIGABRT unknown-type fail-fast, fact-hook retention contract against real persist+engine. wal_commit_mode_t + wal_classify declared in persist.h; inert sentinel stub. 8/8 RED; suite baseline zero preserved. Finding: RETAIN is registration-free (commit.c:233-239) — observable contract documented in Implementation Notes.
This commit is contained in:
parent
8fe2f1f96c
commit
fec9519960
9 changed files with 568 additions and 92 deletions
|
|
@ -121,6 +121,7 @@ controller_unit_test_src = [
|
|||
'testsuite/test_ha_rdhup_integration.c',
|
||||
'testsuite/test_ha_liveness.c',
|
||||
'testsuite/test_commit_engine.c',
|
||||
'testsuite/test_commit_barrier_rollout.c',
|
||||
'commit.c',
|
||||
'startup_validation.c',
|
||||
'worker.c',
|
||||
|
|
|
|||
|
|
@ -1118,6 +1118,18 @@ static uint32_t detail_to_wal_type(int detail)
|
|||
}
|
||||
}
|
||||
|
||||
/* Write-path commit classification — INERT RED-phase stub
|
||||
* (ha-commit-barrier-rollout task 1.1; sanctioned scaffolding so the RED
|
||||
* tests fail on assertions, not at link time). Returns an out-of-enum
|
||||
* sentinel: classifies nothing yet. The verified classification table
|
||||
* (design.md §Write-Path Classification) is implemented in task 1.2. */
|
||||
wal_commit_mode_t wal_classify(uint32_t wal_type, int detail)
|
||||
{
|
||||
(void)wal_type;
|
||||
(void)detail;
|
||||
return (wal_commit_mode_t)-1; /* invalid sentinel — task 1.2 replaces */
|
||||
}
|
||||
|
||||
/* P0 payload: just the detail value (4 bytes).
|
||||
* Full Cap'n Proto serialization is wired in Task 15. */
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ extern void test_ha_connection_reuse(void);
|
|||
extern void test_ha_rdhup_integration(void);
|
||||
extern void test_ha_liveness(void);
|
||||
extern void test_commit_engine(void);
|
||||
extern void test_commit_barrier_rollout(void);
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
||||
|
|
@ -171,6 +172,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_ha_rdhup_integration);
|
||||
TEST(test_ha_liveness);
|
||||
TEST(test_commit_engine);
|
||||
TEST(test_commit_barrier_rollout);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
426
src/controller/testsuite/test_commit_barrier_rollout.c
Normal file
426
src/controller/testsuite/test_commit_barrier_rollout.c
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
/*
|
||||
* test_commit_barrier_rollout.c — RED-phase tests for write-path commit
|
||||
* classification (ha-commit-barrier-rollout task 1.1).
|
||||
*
|
||||
* Covers Requirements 2.1 (every state-changing WAL type explicitly
|
||||
* classified WAIT/RETAIN/LOCAL), 2.2 (unclassified type fails fast under
|
||||
* XCE_TESTING), and 3.1 (fact-shaped appends are retained — appended,
|
||||
* non-blocking, pending purely by lsn > committed_lsn, committed later
|
||||
* by covering ACKs without redelivery).
|
||||
*
|
||||
* The classification table under test is the VERIFIED table from
|
||||
* .kiro/specs/ha-commit-barrier-rollout/design.md §Write-Path
|
||||
* Classification (it supersedes the umbrella draft table — research.md §2
|
||||
* records the call-site-verified deviations: WAL_JOB_STOP/RESUME are
|
||||
* fact-origin only; WAL_JOB_UPDATE/WAL_JOB_FAIL are dual-origin and
|
||||
* classify RETAIN at the seam, with the request-shaped guarantee realized
|
||||
* by the handler barrier in do_jobcontrol_op — tasks 2.x/3.x):
|
||||
*
|
||||
* WAL_JOB_SUBMIT request WAIT
|
||||
* WAL_QUEUE_CTRL request WAIT
|
||||
* WAL_WORKER_CTRL + JOIN/UPDATE request WAIT
|
||||
* WAL_WORKER_CTRL + DEAD fact RETAIN
|
||||
* WAL_JOB_START/FINISH/FAIL/STOP/
|
||||
* RESUME/UPDATE/RUSAGE/CKPT/
|
||||
* PREASON fact RETAIN
|
||||
* WAL_CHECKPOINT local LOCAL (never replicated)
|
||||
* unknown — XCE_TESTING assert
|
||||
*
|
||||
* These cases FAIL (RED) against the task-1.1 inert wal_classify stub in
|
||||
* persist.c (returns an invalid sentinel mode) and go green with the
|
||||
* task-1.2 implementation.
|
||||
*
|
||||
* Honest-observable note for the RETAIN-registration case (task brief):
|
||||
* commit_submit(COMMIT_MODE_RETAIN) is registration-free bookkeeping
|
||||
* (commit.c — returns 0 immediately; commit state is derived entirely
|
||||
* from registry watermarks at ACK time), so RETAIN routing adds NO
|
||||
* engine-side observable distinct from the append itself. The fact-hook
|
||||
* test therefore asserts the full observable retention contract —
|
||||
* classified RETAIN (the RED carrier), hook returns only the local-append
|
||||
* rc without blocking, entry in the WAL with lsn > committed_lsn (pending,
|
||||
* never confirmed early), and committed_lsn advancing over the entry once
|
||||
* a covering ACK arrives (no redelivery) — rather than inventing a fake
|
||||
* registration observable.
|
||||
*
|
||||
* Fixture patterns from test_commit_engine.c (stub persist_t + replicator
|
||||
* with dummy endpoints; real tmpdir persist_t for the hook-driven append;
|
||||
* repl_broadcast_send_hook to drop broadcasts) and test_persist_hooks.c
|
||||
* (variadic trampoline driving persist_job_hook_fun with a real va_list).
|
||||
*
|
||||
* Requirements: 2.1, 2.2, 3.1
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "xce/cluster_def.h" /* HOOK_DETAIL_*, XCE_CONTROLLER_HOOK_JOB */
|
||||
#include "xce/types.h" /* job_t */
|
||||
|
||||
#include "commit.h"
|
||||
#include "hook.h" /* HOOK_AFTER_EVENT */
|
||||
#include "persist.h" /* wal_commit_mode_t, wal_classify (task 1.1 decls) */
|
||||
#include "repl.h"
|
||||
#include "typemap.h" /* wal_catchup_entry_t (send hook signature) */
|
||||
#include "wal_types.h"
|
||||
|
||||
/* Test-injectable send hook (defined in repl.c under XCE_TESTING) — keeps
|
||||
* repl_broadcast from dereferencing the dummy endpoints. */
|
||||
extern int (*repl_broadcast_send_hook)(void *ep,
|
||||
wal_catchup_entry_t **entries,
|
||||
int n_entries);
|
||||
|
||||
static int cbr_drop_send_hook(void *ep, wal_catchup_entry_t **entries,
|
||||
int n_entries)
|
||||
{
|
||||
(void)ep;
|
||||
(void)entries;
|
||||
(void)n_entries;
|
||||
return 0; /* pretend success, drop the data */
|
||||
}
|
||||
|
||||
/* ── Shared helpers ──────────────────────────────────────────────────────── */
|
||||
|
||||
static uint64_t mono_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
/* Valid-mode predicate for the totality sweep (Req 2.1). */
|
||||
static int is_valid_mode(wal_commit_mode_t m)
|
||||
{
|
||||
return m == WAL_MODE_WAIT || m == WAL_MODE_RETAIN || m == WAL_MODE_LOCAL;
|
||||
}
|
||||
|
||||
/* ── Classification table cases (Req 2.1) ─────────────────────────────────
|
||||
* All RED against the inert stub: it returns an out-of-enum sentinel, so
|
||||
* every equality below fails on assertion, not linking. */
|
||||
|
||||
static void test_classify_job_submit_is_wait(void)
|
||||
{
|
||||
/* Arrange / Act */
|
||||
wal_commit_mode_t mode = wal_classify(WAL_JOB_SUBMIT, HOOK_DETAIL_JOB_NEW);
|
||||
|
||||
/* Assert: client job submit is request-shaped — commit barrier before
|
||||
* the requester reply (reference impl keeps the in-hook sync path) */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(WAL_MODE_WAIT, mode,
|
||||
"WAL_JOB_SUBMIT must classify WAIT (request-shaped)");
|
||||
|
||||
printf("test_classify_job_submit_is_wait: PASS\n");
|
||||
}
|
||||
|
||||
static void test_classify_queue_ctrl_is_wait(void)
|
||||
{
|
||||
/* Arrange / Act */
|
||||
wal_commit_mode_t mode = wal_classify(WAL_QUEUE_CTRL,
|
||||
HOOK_DETAIL_QUEUE_UPDATE);
|
||||
|
||||
/* Assert: queue control is client-request-shaped */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(WAL_MODE_WAIT, mode,
|
||||
"WAL_QUEUE_CTRL must classify WAIT (request-shaped)");
|
||||
|
||||
printf("test_classify_queue_ctrl_is_wait: PASS\n");
|
||||
}
|
||||
|
||||
static void test_classify_worker_ctrl_detail_split(void)
|
||||
{
|
||||
/* Arrange / Act: WAL_WORKER_CTRL is the only detail-disambiguated type */
|
||||
wal_commit_mode_t join_mode =
|
||||
wal_classify(WAL_WORKER_CTRL, HOOK_DETAIL_WORKER_JOIN);
|
||||
wal_commit_mode_t update_mode =
|
||||
wal_classify(WAL_WORKER_CTRL, HOOK_DETAIL_WORKER_UPDATE);
|
||||
wal_commit_mode_t dead_mode =
|
||||
wal_classify(WAL_WORKER_CTRL, HOOK_DETAIL_WORKER_DEAD);
|
||||
|
||||
/* Assert: join/update have a requester awaiting confirmation (WAIT);
|
||||
* worker death is controller-detected — a reported fact (RETAIN) */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(WAL_MODE_WAIT, join_mode,
|
||||
"WAL_WORKER_CTRL + WORKER_JOIN must classify WAIT");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(WAL_MODE_WAIT, update_mode,
|
||||
"WAL_WORKER_CTRL + WORKER_UPDATE must classify WAIT");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(WAL_MODE_RETAIN, dead_mode,
|
||||
"WAL_WORKER_CTRL + WORKER_DEAD must classify RETAIN (fact)");
|
||||
|
||||
printf("test_classify_worker_ctrl_detail_split: PASS\n");
|
||||
}
|
||||
|
||||
static void test_classify_all_job_state_types_are_retain(void)
|
||||
{
|
||||
/* Arrange: every WAL_JOB_* state-transition type with its natural hook
|
||||
* detail (verified table: all fact-shaped — STOP/RESUME are fact-origin
|
||||
* only; FAIL/UPDATE dual-origin, request side covered by the handler
|
||||
* barrier in later tasks) */
|
||||
static const struct {
|
||||
uint32_t wal_type;
|
||||
int detail;
|
||||
const char *name;
|
||||
} facts[] = {
|
||||
{ WAL_JOB_START, HOOK_DETAIL_JOB_START, "WAL_JOB_START" },
|
||||
{ WAL_JOB_FINISH, HOOK_DETAIL_JOB_EXIT, "WAL_JOB_FINISH" },
|
||||
{ WAL_JOB_FAIL, HOOK_DETAIL_JOB_FAIL, "WAL_JOB_FAIL" },
|
||||
{ WAL_JOB_STOP, HOOK_DETAIL_JOB_STOP, "WAL_JOB_STOP" },
|
||||
{ WAL_JOB_RESUME, HOOK_DETAIL_JOB_RESUME, "WAL_JOB_RESUME" },
|
||||
{ WAL_JOB_UPDATE, HOOK_DETAIL_JOB_UPDATE, "WAL_JOB_UPDATE" },
|
||||
{ WAL_JOB_RUSAGE, HOOK_DETAIL_JOB_RUSAGE_UPDATE, "WAL_JOB_RUSAGE" },
|
||||
{ WAL_JOB_CKPT, HOOK_DETAIL_JOB_CKPT, "WAL_JOB_CKPT" },
|
||||
{ WAL_JOB_PREASON, HOOK_DETAIL_SETPREASON, "WAL_JOB_PREASON" },
|
||||
};
|
||||
size_t i;
|
||||
|
||||
/* Act + Assert: each is a reported fact — retained until committed */
|
||||
for (i = 0; i < sizeof(facts) / sizeof(facts[0]); i++) {
|
||||
char msg[96];
|
||||
|
||||
snprintf(msg, sizeof(msg), "%s must classify RETAIN (fact-shaped)",
|
||||
facts[i].name);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
WAL_MODE_RETAIN, wal_classify(facts[i].wal_type, facts[i].detail),
|
||||
msg);
|
||||
}
|
||||
|
||||
printf("test_classify_all_job_state_types_are_retain: PASS\n");
|
||||
}
|
||||
|
||||
static void test_classify_checkpoint_is_local(void)
|
||||
{
|
||||
/* Arrange / Act: WAL_CHECKPOINT is a local WAL marker, never replicated */
|
||||
wal_commit_mode_t mode = wal_classify(WAL_CHECKPOINT, 0);
|
||||
|
||||
/* Assert */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(WAL_MODE_LOCAL, mode,
|
||||
"WAL_CHECKPOINT must classify LOCAL (never replicated)");
|
||||
|
||||
printf("test_classify_checkpoint_is_local: PASS\n");
|
||||
}
|
||||
|
||||
static void test_classify_is_total_over_wal_entry_type(void)
|
||||
{
|
||||
/* Arrange: EVERY value of wal_entry_type (wal_types.h:44-58, verified
|
||||
* 2026-06-11: 13 values, no WAL_NOOP yet). When a new type is added,
|
||||
* this sweep plus the unknown-type guard force it to be classified. */
|
||||
static const struct {
|
||||
uint32_t wal_type;
|
||||
int detail;
|
||||
const char *name;
|
||||
} all_types[] = {
|
||||
{ WAL_JOB_SUBMIT, HOOK_DETAIL_JOB_NEW, "WAL_JOB_SUBMIT" },
|
||||
{ WAL_JOB_START, HOOK_DETAIL_JOB_START, "WAL_JOB_START" },
|
||||
{ WAL_JOB_FINISH, HOOK_DETAIL_JOB_EXIT, "WAL_JOB_FINISH" },
|
||||
{ WAL_JOB_FAIL, HOOK_DETAIL_JOB_FAIL, "WAL_JOB_FAIL" },
|
||||
{ WAL_JOB_STOP, HOOK_DETAIL_JOB_STOP, "WAL_JOB_STOP" },
|
||||
{ WAL_JOB_RESUME, HOOK_DETAIL_JOB_RESUME, "WAL_JOB_RESUME" },
|
||||
{ WAL_JOB_UPDATE, HOOK_DETAIL_JOB_UPDATE, "WAL_JOB_UPDATE" },
|
||||
{ WAL_JOB_RUSAGE, HOOK_DETAIL_JOB_RUSAGE_UPDATE, "WAL_JOB_RUSAGE" },
|
||||
{ WAL_JOB_CKPT, HOOK_DETAIL_JOB_CKPT, "WAL_JOB_CKPT" },
|
||||
{ WAL_JOB_PREASON, HOOK_DETAIL_CLRPREASON, "WAL_JOB_PREASON" },
|
||||
{ WAL_QUEUE_CTRL, HOOK_DETAIL_QUEUE_UPDATE, "WAL_QUEUE_CTRL" },
|
||||
{ WAL_WORKER_CTRL, HOOK_DETAIL_WORKER_DEAD, "WAL_WORKER_CTRL" },
|
||||
{ WAL_CHECKPOINT, 0, "WAL_CHECKPOINT" },
|
||||
};
|
||||
size_t i;
|
||||
|
||||
/* Act + Assert: classification is total — every enum value maps to a
|
||||
* valid mode (no silent barrier bypass for any state-changing type) */
|
||||
for (i = 0; i < sizeof(all_types) / sizeof(all_types[0]); i++) {
|
||||
char msg[96];
|
||||
|
||||
snprintf(msg, sizeof(msg),
|
||||
"%s must map to a valid commit mode (totality)",
|
||||
all_types[i].name);
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
is_valid_mode(wal_classify(all_types[i].wal_type,
|
||||
all_types[i].detail)),
|
||||
msg);
|
||||
}
|
||||
|
||||
printf("test_classify_is_total_over_wal_entry_type: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Unknown type fails fast under XCE_TESTING (Req 2.2) ──────────────────
|
||||
* The guard contract is assert(0) in testing builds (abort), production
|
||||
* ERROR + RETAIN fallback. The abort is observed from a forked child so
|
||||
* the test runner survives (fork pattern precedent: lib testsuite test.c).
|
||||
* RED: the inert stub returns its sentinel without asserting, so the child
|
||||
* exits normally and the SIGABRT assertion fails. */
|
||||
static void test_classify_unknown_type_fails_fast_in_testing(void)
|
||||
{
|
||||
pid_t pid;
|
||||
int status = 0;
|
||||
|
||||
/* Arrange: a WAL type far outside wal_entry_type */
|
||||
const uint32_t unknown_type = 777u;
|
||||
|
||||
/* Act: evaluate the unknown type in a child process */
|
||||
pid = fork();
|
||||
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork must succeed");
|
||||
if (pid == 0) {
|
||||
/* Child: silence the expected assert/abort noise, then classify.
|
||||
* Reaching _exit(0) means the fail-fast path did NOT trigger. */
|
||||
int devnull = open("/dev/null", O_WRONLY);
|
||||
if (devnull >= 0) {
|
||||
dup2(devnull, STDERR_FILENO);
|
||||
dup2(devnull, STDOUT_FILENO);
|
||||
}
|
||||
(void)wal_classify(unknown_type, 0);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
/* Assert: the child must die on the XCE_TESTING fail-fast path */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(pid, waitpid(pid, &status, 0),
|
||||
"waitpid must reap the classifier child");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT,
|
||||
"unknown WAL type must trip the XCE_TESTING fail-fast (abort)");
|
||||
|
||||
printf("test_classify_unknown_type_fails_fast_in_testing: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Fact-type append through the hook seam is RETAIN-routed (Req 3.1) ────
|
||||
* Drives the production persist_job_hook_fun with a worker-reported job
|
||||
* finish (fact) against a real tmpdir persist_t with a wired commit engine
|
||||
* (n_peers=3) and ACKs withheld. Observable retention contract:
|
||||
* - wal_classify routes the type RETAIN (RED carrier — fails against the
|
||||
* inert stub; the behavioral assertions below pass on unmodified
|
||||
* persist.c and pin the contract task 1.2 must preserve)
|
||||
* - the hook returns the local-append rc (0) WITHOUT blocking on commit
|
||||
* progress (no timeout applies to facts)
|
||||
* - the entry is in the WAL and pending purely by lsn > committed_lsn
|
||||
* (never confirmed early)
|
||||
* - a later covering ACK advances committed_lsn over the entry with no
|
||||
* redelivery of the fact
|
||||
* commit_submit(RETAIN) itself is registration-free (commit.c) — see the
|
||||
* file header note; there is deliberately no fake "registered" observable. */
|
||||
|
||||
/* Variadic trampoline so the test passes typed args through a real va_list
|
||||
* (pattern from test_persist_hooks.c). */
|
||||
static int call_job_hook(int type, int sit, int detail, persist_t *p, ...)
|
||||
{
|
||||
va_list ap;
|
||||
int rc;
|
||||
|
||||
va_start(ap, p);
|
||||
rc = persist_job_hook_fun(type, sit, detail, p, ap);
|
||||
va_end(ap);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Statics survive Unity's longjmp on assertion failure. */
|
||||
static persist_t *g_fp;
|
||||
static commit_engine_t g_fce;
|
||||
static job_t g_fact_job;
|
||||
static int g_fep_a, g_fep_b; /* dummy endpoints — replicator compares pointers */
|
||||
|
||||
static void test_fact_hook_append_is_retained_and_nonblocking(void)
|
||||
{
|
||||
persist_config_t cfg;
|
||||
char tmpdir[256];
|
||||
uint64_t t0, elapsed_ms, fact_lsn;
|
||||
int rc;
|
||||
|
||||
/* Arrange: real persist over a private tmpdir; sync timeout large so a
|
||||
* misrouted blocking wait is detectable as elapsed time */
|
||||
cfg = persist_config_defaults();
|
||||
cfg.wal_sync_ms = 5000;
|
||||
cfg.sync_timeout_ms = 3000;
|
||||
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/xce_cbr11_fact_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmpdir));
|
||||
|
||||
g_fp = calloc(1, sizeof(persist_t));
|
||||
TEST_ASSERT_NOT_NULL(g_fp);
|
||||
persist_set_workdir(g_fp, tmpdir, "ctrl");
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_init(g_fp, &cfg, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_start(g_fp));
|
||||
|
||||
/* 3-member cluster: 2 standbys registered, engine wired, ACKs withheld */
|
||||
TEST_ASSERT_TRUE(repl_add_standby(&g_fp->repl, &g_fep_a, "sb_a", 7200) >= 0);
|
||||
TEST_ASSERT_TRUE(repl_add_standby(&g_fp->repl, &g_fep_b, "sb_b", 7201) >= 0);
|
||||
TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&g_fce, g_fp, 3, 8));
|
||||
repl_set_commit_engine(&g_fp->repl, &g_fce);
|
||||
repl_broadcast_send_hook = cbr_drop_send_hook;
|
||||
|
||||
/* Priming async append so the fact entry lands at LSN 1 (the engine
|
||||
* rejects lsn == 0 — accepted interim LSN-0 edge, ha-commit-engine) */
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(g_fp, WAL_JOB_UPDATE, "p", 1));
|
||||
fact_lsn = g_fp->current_lsn; /* the LSN the fact append will get */
|
||||
TEST_ASSERT_EQUAL_UINT64(1, fact_lsn);
|
||||
|
||||
memset(&g_fact_job, 0, sizeof(g_fact_job));
|
||||
g_fact_job.job.jobid = 42;
|
||||
g_fact_job.job.state = 0x10;
|
||||
|
||||
/* Act: worker-reported job finish (fact) through the production hook.
|
||||
* JOB_EXIT AFTER_EVENT args: cluster_t*, xce_jobfinish_t*, job_t* */
|
||||
t0 = mono_ms();
|
||||
rc = call_job_hook(XCE_CONTROLLER_HOOK_JOB, HOOK_AFTER_EVENT,
|
||||
HOOK_DETAIL_JOB_EXIT, g_fp,
|
||||
(void *)NULL, (void *)NULL, &g_fact_job);
|
||||
elapsed_ms = mono_ms() - t0;
|
||||
|
||||
/* Assert: the seam classifies the fact RETAIN (RED carrier — the routing
|
||||
* contract the behavioral assertions below depend on) */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
WAL_MODE_RETAIN, wal_classify(WAL_JOB_FINISH, HOOK_DETAIL_JOB_EXIT),
|
||||
"fact-type WAL_JOB_FINISH must be RETAIN-routed at the hook seam");
|
||||
|
||||
/* ...the hook propagated only the local-append rc, without blocking on
|
||||
* commit progress (facts have no commit timeout) */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
|
||||
"fact hook must return the local-append rc only (no commit error)");
|
||||
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1000,
|
||||
"fact append must not block on commit progress (no timeout applies)");
|
||||
|
||||
/* ...the entry is in the WAL, pending purely by lsn > committed_lsn */
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(fact_lsn + 1, g_fp->current_lsn,
|
||||
"the fact entry must be appended to the WAL");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
atomic_load(&g_fp->committed_lsn) < fact_lsn,
|
||||
"the uncommitted fact must be pending (lsn > committed_lsn), "
|
||||
"never confirmed early");
|
||||
|
||||
/* ...and a covering ACK later commits the retained entry — engine
|
||||
* accounting advances committed_lsn over it, no redelivery needed */
|
||||
repl_on_ack(&g_fp->repl, &g_fep_a, fact_lsn);
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
||||
fact_lsn, atomic_load(&g_fp->committed_lsn),
|
||||
"a covering ACK must advance committed_lsn over the retained fact");
|
||||
|
||||
/* Teardown */
|
||||
repl_broadcast_send_hook = NULL;
|
||||
commit_engine_fini(&g_fce);
|
||||
persist_stop(g_fp);
|
||||
persist_fini(g_fp);
|
||||
free(g_fp);
|
||||
g_fp = NULL;
|
||||
|
||||
printf("test_fact_hook_append_is_retained_and_nonblocking: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test group entry point ──────────────────────────────────────────────── */
|
||||
|
||||
void test_commit_barrier_rollout(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
/* Task 1.1 — classification totality + RETAIN routing (Req 2.1, 2.2, 3.1) */
|
||||
RUN_TEST(test_classify_job_submit_is_wait);
|
||||
RUN_TEST(test_classify_queue_ctrl_is_wait);
|
||||
RUN_TEST(test_classify_worker_ctrl_detail_split);
|
||||
RUN_TEST(test_classify_all_job_state_types_are_retain);
|
||||
RUN_TEST(test_classify_checkpoint_is_local);
|
||||
RUN_TEST(test_classify_is_total_over_wal_entry_type);
|
||||
RUN_TEST(test_classify_unknown_type_fails_fast_in_testing);
|
||||
RUN_TEST(test_fact_hook_append_is_retained_and_nonblocking);
|
||||
UNITY_END();
|
||||
}
|
||||
|
|
@ -116,6 +116,21 @@ int persist_wal_sync_append(persist_t* p, uint32_t type,
|
|||
const void* payload, uint32_t len,
|
||||
int* sync_rc_out);
|
||||
|
||||
/* ============================================================
|
||||
* Write-path commit classification (ha-commit-barrier-rollout)
|
||||
* ============================================================ */
|
||||
typedef enum {
|
||||
WAL_MODE_WAIT, /* request-shaped: barrier before requester reply */
|
||||
WAL_MODE_RETAIN, /* fact-shaped: retained until committed, no timeout */
|
||||
WAL_MODE_LOCAL, /* not replicated (WAL_CHECKPOINT) */
|
||||
} wal_commit_mode_t;
|
||||
|
||||
/* Pure classification lookup. detail disambiguates WAL_WORKER_CTRL only
|
||||
* (HOOK_DETAIL_WORKER_JOIN/UPDATE -> WAIT, HOOK_DETAIL_WORKER_DEAD ->
|
||||
* RETAIN). Unknown type: XCE_TESTING assert (fail fast); production logs
|
||||
* ERROR and returns WAL_MODE_RETAIN (never dropped, never confirmed early). */
|
||||
wal_commit_mode_t wal_classify(uint32_t wal_type, int detail);
|
||||
|
||||
/* Force WAL fdatasync (for tests and clean shutdown) */
|
||||
int persist_flush(persist_t* p);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue