feat(ha-convergence-recovery): commit-gated scheduler egress — submit_lsn tagging + watermark gate (task 1.2)

This commit is contained in:
Rongsong Shen 2026-06-14 02:33:36 +00:00
parent 7ff81156c2
commit b9ddfa60cb
9 changed files with 241 additions and 67 deletions

View file

@ -1,6 +1,6 @@
/*
* test_egress_filter.c — RED-phase tests for commit-gated scheduler egress
* (ha-convergence-recovery task 1.1).
* test_egress_filter.c — tests for commit-gated scheduler egress
* (ha-convergence-recovery tasks 1.1 RED / 1.2 GREEN).
*
* Requirements (this spec's IDs):
* 3.1 — a job whose submit entry is not yet committed is EXCLUDED from
@ -15,15 +15,14 @@
* status/API read path (plain collect_jobs is NOT commit-filtered).
* submit_lsn == 0 is always exportable (pre-feature / unknown).
*
* RED CONTRACT: these tests are written against the task-1.2 GREEN behavior.
* They FAIL today because:
* - persist_job_hook_fun does not yet set j->submit_lsn (live tagging gap)
* - apply_wal_entry passes 0 (not hdr->lsn) to restore_job, and restore_job
* does not tag submit_lsn (replay tagging gap)
* - job_submit_committed / collect_jobs_committed are not yet implemented in
* dataex.c (the RED scaffold below is intentionally unfiltered — task 1.2
* DELETES this scaffold and implements the real predicate + wrapper in
* dataex.c per design.md "Egress Filter").
* GREEN (task 1.2): the production tagging + watermark gate are now wired and
* these tests exercise the REAL symbols:
* - persist_job_hook_fun sets j->submit_lsn = written_lsn on live submit
* - apply_wal_entry threads hdr->lsn to restore_job, which tags submit_lsn
* - job_submit_committed / collect_jobs_committed live in egress.c (linked
* into this binary) per design.md "Egress Filter" (D3 — factored out of
* dataex.c so the unit test can link the real predicate without dataex.c's
* notify()/collect_confvars() clashing with existing test stubs).
*
* Harness: a real persist_t over a private tmpdir embedded in a test
* cluster_t with a registered jobmgr and persistence hooks, a wired commit
@ -60,15 +59,19 @@
#include "wal_types.h"
#include "xlist.h"
/* ── Local job collector (avoids linking dataex.c into this unit binary) ──
/* ── Status/API read-path collector (unfiltered — Req 3.4) ───────────────
*
* dataex.c is NOT in controller_unit_test_src (its notify()/collect_confvars()
* clash with stubs already in test_commit_barrier_reply.c / test_worker_join.c).
* collect_jobs / unlock_collect_jobs therefore are not linkable here, so this
* file uses ef_collect_jobs — a faithful copy of collect_jobs' jobmgr iteration
* (dataex.c:294) — for the unfiltered status/API path AND as the iteration core
* of the RED scaffold below. The behavior asserted is identical to collect_jobs;
* only the symbol is local. */
* The real scheduler-bound predicate + wrapper (job_submit_committed,
* collect_jobs_committed) now live in egress.c, which IS linked into this test
* binary (controller_unit_test_src) — these tests exercise the REAL production
* symbols. dataex.c (which owns plain collect_jobs / unlock_collect_jobs) is
* still NOT linked here: its notify()/collect_confvars() definitions clash with
* stubs already provided by test_commit_barrier_reply.c / test_worker_join.c.
* For the unfiltered status-path assertion (3.4) this file therefore keeps a
* faithful local copy of collect_jobs' iteration — ef_collect_jobs — and a
* matching ef_unlock_jobs (reference/dereference only, no per-job lock, since
* ef_collect_jobs does not lock). The behavior asserted is identical to
* collect_jobs; only the symbol is local. */
static void ef_collect_jobs(cluster_t *c, xlist_t *l)
{
iterator_t it;
@ -103,35 +106,22 @@ static void ef_unlock_jobs(xlist_t *l)
}
}
/* ── RED scaffold: minimal, intentionally UNFILTERED stand-ins ───────────
*
* These satisfy the linker (job_submit_committed / collect_jobs_committed are
* declared in cluster.h but not yet defined). They DO NOT implement the commit
* watermark — job_submit_committed always returns true and collect_jobs_committed
* collects every job. That makes the 3.1/3.2 exclusion assertions FAIL (the
* uncommitted job leaks through), which is the intended RED state.
*
* Task 1.2 (GREEN) MUST delete this entire scaffold block and provide the real
* definitions in dataex.c per design.md "Egress Filter":
* job_submit_committed -> submit_lsn == 0 || submit_lsn <= committed_lsn
* collect_jobs_committed -> collect_jobs filtered by that predicate
* At that point dataex.c becomes part of the test link (with the notify/
* collect_confvars stub clashes reconciled) and this scaffold is removed to
* avoid duplicate symbols. */
bool job_submit_committed(cluster_t *c, job_t *j)
/* Release a list produced by the REAL collect_jobs_committed (egress.c): it
* read-locks each collected job (identical contract to collect_jobs), so the
* matching release must unlock AND dereference — mirror of the production
* unlock_collect_jobs (dataex.c), which is not linkable in this binary. */
static void ef_unlock_committed(xlist_t *l)
{
(void)c;
(void)j;
return true; /* RED: unfiltered — task 1.2 replaces with the watermark */
for (int i = 0; i < l->count; i++) {
xce_job_t *xj = (xce_job_t *)xlist_get(l, i);
if (xj == NULL)
continue;
job_t *j = CONTAINER_OF(xj, job_t, job);
unlock(j);
dereference(j);
}
}
void collect_jobs_committed(cluster_t *c, collect_check_fn_t fn, xlist_t *l)
{
(void)fn; /* scheduler-bound callers pass NULL */
ef_collect_jobs(c, l); /* RED: no commit filtering */
}
/* ── end RED scaffold ──────────────────────────────────────────────────── */
/* notify() is provided by test_commit_barrier_reply.c (same test binary) — do
* not redefine it here (would be a duplicate symbol). */
@ -423,7 +413,7 @@ static void test_uncommitted_job_absent_from_scheduler_export(void)
"uncommitted job (submit_lsn=9 > committed_lsn=5) leaked into "
"scheduler export");
ef_unlock_jobs(&out);
ef_unlock_committed(&out);
xlist_fini(&out);
ef_teardown();
printf("test_uncommitted_job_absent_from_scheduler_export: checked\n");
@ -449,7 +439,7 @@ static void test_job_present_after_commit_advances(void)
TEST_ASSERT_EQUAL_INT(0, xlist_init(&before, 16));
collect_jobs_committed(&g_c, NULL, &before);
TEST_ASSERT_FALSE(ef_list_has_job(&before, 404));
ef_unlock_jobs(&before);
ef_unlock_committed(&before);
xlist_fini(&before);
/* Advance committed_lsn to cover the submit entry. */
@ -465,7 +455,7 @@ static void test_job_present_after_commit_advances(void)
ef_list_has_job(&after, 404),
"job absent from scheduler export after committed_lsn advanced to "
"cover its submit entry");
ef_unlock_jobs(&after);
ef_unlock_committed(&after);
xlist_fini(&after);
ef_teardown();
@ -489,7 +479,7 @@ static void test_submit_lsn_zero_always_exportable(void)
TEST_ASSERT_EQUAL_INT(0, xlist_init(&out, 16));
collect_jobs_committed(&g_c, NULL, &out);
TEST_ASSERT_TRUE(ef_list_has_job(&out, 505));
ef_unlock_jobs(&out);
ef_unlock_committed(&out);
xlist_fini(&out);
ef_teardown();
@ -526,7 +516,7 @@ static void test_uncommitted_job_visible_to_status_path(void)
void test_egress_filter(void)
{
printf("\n>> commit-gated scheduler egress tests (RED, task 1.1)\n\n");
printf("\n>> commit-gated scheduler egress tests (GREEN, task 1.2)\n\n");
RUN_TEST(test_live_submit_tags_submit_lsn);
RUN_TEST(test_replay_restore_tags_entry_lsn);

View file

@ -28,9 +28,9 @@ typedef struct cluster_s cluster_t;
__attribute__((weak))
int restore_job(cluster_t* c, uint64_t jobid,
const void* capnp_payload, size_t len)
const void* capnp_payload, size_t len, uint64_t entry_lsn)
{
(void)c; (void)jobid; (void)capnp_payload; (void)len;
(void)c; (void)jobid; (void)capnp_payload; (void)len; (void)entry_lsn;
return 0;
}