test(raft-ha-core-safety): machine-checkable walcompare report (task 1.1)
Per-member (lsn,term,pcrc,present) report + committed-vs-uncommitted divergence classification via walcompare_report()/--report. Soak signal (OK/exit-code) preserved. 4 new unit tests, all GREEN.
This commit is contained in:
parent
227f592250
commit
b028d25dbd
5 changed files with 453 additions and 120 deletions
|
|
@ -7,7 +7,7 @@
|
||||||
> **Hypothesis discipline:** the term-blind bypass is the primary hypothesis, not proven sole cause. Task 1.3 must reproduce the *live* divergence first; a green synthetic unit test over a still-red soak does not satisfy 1.1/8.1.
|
> **Hypothesis discipline:** the term-blind bypass is the primary hypothesis, not proven sole cause. Task 1.3 must reproduce the *live* divergence first; a green synthetic unit test over a still-red soak does not satisfy 1.1/8.1.
|
||||||
|
|
||||||
- [ ] 1. Foundation: deterministic chaotic harness and live-divergence reproduction
|
- [ ] 1. Foundation: deterministic chaotic harness and live-divergence reproduction
|
||||||
- [ ] 1.1 Extend the divergence detector to a machine-checkable per-member report
|
- [x] 1.1 Extend the divergence detector to a machine-checkable per-member report
|
||||||
- Extend `walcompare` to emit a parseable per-member `(lsn -> term, payload-crc, present/MISSING)` report over the committed prefix, and classify each divergence as committed vs uncommitted
|
- Extend `walcompare` to emit a parseable per-member `(lsn -> term, payload-crc, present/MISSING)` report over the committed prefix, and classify each divergence as committed vs uncommitted
|
||||||
- Observable completion: `walcompare` prints a parseable per-member report and a committed-vs-uncommitted classification; the existing soak still detects the round-3 LSN divergence through it
|
- Observable completion: `walcompare` prints a parseable per-member report and a committed-vs-uncommitted classification; the existing soak still detects the round-3 LSN divergence through it
|
||||||
- _Requirements: 7.1, 7.5_
|
- _Requirements: 7.1, 7.5_
|
||||||
|
|
|
||||||
|
|
@ -721,41 +721,54 @@ int cmd_waldump(int argc, char* argv[])
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* walcompare [--committed N] <memberA> <memberB> [more...] — diff member WAL
|
/* walcompare [--report] [--committed N] <memberA> <memberB> [more...] — diff
|
||||||
* committed prefixes by (lsn -> term, pcrc) and FAIL (nonzero) on any
|
* member WAL committed prefixes by (lsn -> term, pcrc) and FAIL (nonzero) on any
|
||||||
* committed-prefix divergence (ha-term-safety R6.1/R6.2/R6.3), reusing the
|
* committed-prefix divergence (ha-term-safety R6.1/R6.2/R6.3), reusing the
|
||||||
* waldump decoder. Each member path may be a seg-*.wal file, a wal/ dir, or a
|
* waldump decoder. Each member path may be a seg-*.wal file, a wal/ dir, or a
|
||||||
* node/data dir (same resolution as waldump). With no --committed, the shared
|
* node/data dir (same resolution as waldump). With no --committed, the shared
|
||||||
* committed boundary = the minimum CRC-intact tip across members. The diverging
|
* committed boundary = the minimum CRC-intact tip across members. The diverging
|
||||||
* LSN and per-member (term, pcrc) identities are reported. Exit 0 = identical,
|
* LSN and per-member (term, pcrc) identities are reported. With --report, also
|
||||||
* nonzero = divergence or load error — so it doubles as a scripted assertion. */
|
* emit a machine-checkable per-member `(lsn, term, pcrc, status)` table and a
|
||||||
|
* committed-vs-uncommitted classification for each divergence (task 1.1). Exit
|
||||||
|
* 0 = identical, nonzero = divergence or load error — so it doubles as a
|
||||||
|
* scripted assertion. */
|
||||||
int cmd_walcompare(int argc, char* argv[])
|
int cmd_walcompare(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
uint64_t committed = 0; /* 0 = derive the shared boundary */
|
uint64_t committed = 0; /* 0 = derive the shared boundary */
|
||||||
|
int report = 0; /* --report => full per-member machine table */
|
||||||
int ai = 1;
|
int ai = 1;
|
||||||
|
|
||||||
/* Optional global committed boundary applied to every member. */
|
/* Optional flags (any order): --report, --committed N. */
|
||||||
if (argc > ai && strcmp(argv[ai], "--committed") == 0) {
|
for (;;) {
|
||||||
if (argc <= ai + 1) {
|
if (argc > ai && strcmp(argv[ai], "--report") == 0) {
|
||||||
fprintf(stderr, "walcompare: --committed needs an LSN\n");
|
report = 1;
|
||||||
return -1;
|
ai += 1;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
char* end = NULL;
|
if (argc > ai && strcmp(argv[ai], "--committed") == 0) {
|
||||||
unsigned long v = strtoul(argv[ai + 1], &end, 10);
|
if (argc <= ai + 1) {
|
||||||
if (end == argv[ai + 1] || *end != '\0') {
|
fprintf(stderr, "walcompare: --committed needs an LSN\n");
|
||||||
fprintf(stderr, "walcompare: bad --committed LSN '%s'\n",
|
return -1;
|
||||||
argv[ai + 1]);
|
}
|
||||||
return -1;
|
char* end = NULL;
|
||||||
|
unsigned long v = strtoul(argv[ai + 1], &end, 10);
|
||||||
|
if (end == argv[ai + 1] || *end != '\0') {
|
||||||
|
fprintf(stderr, "walcompare: bad --committed LSN '%s'\n",
|
||||||
|
argv[ai + 1]);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
committed = (uint64_t)v;
|
||||||
|
ai += 2;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
committed = (uint64_t)v;
|
break;
|
||||||
ai += 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int n = argc - ai;
|
int n = argc - ai;
|
||||||
if (n < 2) {
|
if (n < 2) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"usage: walcompare [--committed N] <memberA> <memberB> "
|
"usage: walcompare [--report] [--committed N] <memberA> "
|
||||||
"[more...]\n");
|
"<memberB> [more...]\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (n > 16) {
|
if (n > 16) {
|
||||||
|
|
@ -770,7 +783,8 @@ int cmd_walcompare(int argc, char* argv[])
|
||||||
}
|
}
|
||||||
|
|
||||||
walcompare_divergence_t div;
|
walcompare_divergence_t div;
|
||||||
int rc = walcompare(members, n, stdout, &div);
|
int rc = report ? walcompare_report(members, n, stdout, &div)
|
||||||
|
: walcompare(members, n, stdout, &div);
|
||||||
if (rc == WALCMP_OK)
|
if (rc == WALCMP_OK)
|
||||||
return 0;
|
return 0;
|
||||||
if (rc == WALCMP_DIVERGENT) {
|
if (rc == WALCMP_DIVERGENT) {
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,160 @@ static void test_too_few_members_errors(void)
|
||||||
TEST_ASSERT_LESS_THAN_INT(0, rc);
|
TEST_ASSERT_LESS_THAN_INT(0, rc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Machine-checkable report: a committed-prefix divergence must emit a */
|
||||||
|
/* parseable per-member line set and a committed-vs-uncommitted */
|
||||||
|
/* classification (task 1.1, R7.1/R7.5). Captured via open_memstream so */
|
||||||
|
/* the exact greppable tokens are asserted, not just the verdict. */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
static void test_machine_report_committed_divergence(void)
|
||||||
|
{
|
||||||
|
wal_spec_t a_specs[] = {
|
||||||
|
{ 1, WAL_NOOP, NULL },
|
||||||
|
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||||
|
{ 2, WAL_JOB_SUBMIT, "jobB" }, /* LSN 3, term 2 */
|
||||||
|
};
|
||||||
|
wal_spec_t b_specs[] = {
|
||||||
|
{ 1, WAL_NOOP, NULL },
|
||||||
|
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||||
|
{ 3, WAL_JOB_SUBMIT, "jobB" }, /* LSN 3, term 3 — divergent */
|
||||||
|
};
|
||||||
|
char a[256], b[256];
|
||||||
|
build_member(a, sizeof(a), a_specs, 3);
|
||||||
|
build_member(b, sizeof(b), b_specs, 3);
|
||||||
|
|
||||||
|
char* buf = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
FILE* out = open_memstream(&buf, &len);
|
||||||
|
TEST_ASSERT_NOT_NULL(out);
|
||||||
|
|
||||||
|
walcompare_member_t m[2] = { { a, 0 }, { b, 0 } };
|
||||||
|
walcompare_divergence_t div;
|
||||||
|
int rc = walcompare(m, 2, out, &div);
|
||||||
|
fclose(out);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
|
||||||
|
/* committed-vs-uncommitted classification line. */
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP_DIVERGENCE"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "class=committed"));
|
||||||
|
/* parseable per-member lines with fixed field order. */
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=0 lsn=3"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=1 lsn=3"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "status=present"));
|
||||||
|
/* divergence struct carries the committed classification flag. */
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, div.committed);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* walcompare_report emits a full per-member, per-committed-LSN table */
|
||||||
|
/* (lsn, term, pcrc, status) for identical members and a clean verdict. */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
static void test_report_full_member_table(void)
|
||||||
|
{
|
||||||
|
wal_spec_t specs[] = {
|
||||||
|
{ 1, WAL_NOOP, NULL },
|
||||||
|
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||||
|
{ 2, WAL_JOB_SUBMIT, "jobB" },
|
||||||
|
};
|
||||||
|
char a[256], b[256], c[256];
|
||||||
|
build_member(a, sizeof(a), specs, 3);
|
||||||
|
build_member(b, sizeof(b), specs, 3);
|
||||||
|
build_member(c, sizeof(c), specs, 3);
|
||||||
|
|
||||||
|
char* buf = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
FILE* out = open_memstream(&buf, &len);
|
||||||
|
TEST_ASSERT_NOT_NULL(out);
|
||||||
|
|
||||||
|
walcompare_member_t m[3] = { { a, 0 }, { b, 0 }, { c, 0 } };
|
||||||
|
walcompare_divergence_t div;
|
||||||
|
int rc = walcompare_report(m, 3, out, &div);
|
||||||
|
fclose(out);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc);
|
||||||
|
/* every member appears at every committed LSN with committed=1. */
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=0 lsn=1"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "WALCMP member=2 lsn=3"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "committed=1"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "status=present"));
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* A MISSING committed entry is tagged status=MISSING in the report, */
|
||||||
|
/* and classified class=committed (a committed-prefix hole, R4.2/R6.4). */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
static void test_report_missing_tagged(void)
|
||||||
|
{
|
||||||
|
wal_spec_t a_specs[] = {
|
||||||
|
{ 1, WAL_NOOP, NULL },
|
||||||
|
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||||
|
{ 2, WAL_JOB_SUBMIT, "jobB" },
|
||||||
|
};
|
||||||
|
wal_spec_t b_specs[] = {
|
||||||
|
{ 1, WAL_NOOP, NULL },
|
||||||
|
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||||
|
};
|
||||||
|
char a[256], b[256];
|
||||||
|
build_member(a, sizeof(a), a_specs, 3);
|
||||||
|
build_member(b, sizeof(b), b_specs, 2);
|
||||||
|
|
||||||
|
char* buf = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
FILE* out = open_memstream(&buf, &len);
|
||||||
|
TEST_ASSERT_NOT_NULL(out);
|
||||||
|
|
||||||
|
walcompare_member_t m[2] = { { a, 3 }, { b, 3 } };
|
||||||
|
walcompare_divergence_t div;
|
||||||
|
int rc = walcompare_report(m, 2, out, &div);
|
||||||
|
fclose(out);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(WALCMP_DIVERGENT, rc);
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "status=MISSING"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "class=committed"));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, div.committed);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* A divergence in the UNCOMMITTED tail (above the committed boundary) is */
|
||||||
|
/* reported class=uncommitted and is NON-fatal: the committed verdict is */
|
||||||
|
/* still WALCMP_OK (task 1.1 classification; §6.1 committed-prefix only). */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
static void test_report_uncommitted_tail_classified(void)
|
||||||
|
{
|
||||||
|
wal_spec_t a_specs[] = {
|
||||||
|
{ 1, WAL_NOOP, NULL },
|
||||||
|
{ 1, WAL_JOB_SUBMIT, "jobA" }, /* committed LSN 2 */
|
||||||
|
{ 2, WAL_JOB_SUBMIT, "uncA" }, /* LSN 3 uncommitted, divergent */
|
||||||
|
};
|
||||||
|
wal_spec_t b_specs[] = {
|
||||||
|
{ 1, WAL_NOOP, NULL },
|
||||||
|
{ 1, WAL_JOB_SUBMIT, "jobA" },
|
||||||
|
{ 9, WAL_JOB_SUBMIT, "uncB" }, /* LSN 3, different term+bytes */
|
||||||
|
};
|
||||||
|
char a[256], b[256];
|
||||||
|
build_member(a, sizeof(a), a_specs, 3);
|
||||||
|
build_member(b, sizeof(b), b_specs, 3);
|
||||||
|
|
||||||
|
char* buf = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
FILE* out = open_memstream(&buf, &len);
|
||||||
|
TEST_ASSERT_NOT_NULL(out);
|
||||||
|
|
||||||
|
/* committed boundary pinned at 2 → LSN-3 divergence is the tail. */
|
||||||
|
walcompare_member_t m[2] = { { a, 2 }, { b, 2 } };
|
||||||
|
walcompare_divergence_t div;
|
||||||
|
int rc = walcompare_report(m, 2, out, &div);
|
||||||
|
fclose(out);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(WALCMP_OK, rc); /* tail divergence is non-fatal */
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "class=uncommitted"));
|
||||||
|
TEST_ASSERT_NOT_NULL(strstr(buf, "committed=0"));
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
void test_walcompare(void)
|
void test_walcompare(void)
|
||||||
{
|
{
|
||||||
RUN_TEST(test_identical_members_ok);
|
RUN_TEST(test_identical_members_ok);
|
||||||
|
|
@ -367,4 +521,8 @@ void test_walcompare(void)
|
||||||
RUN_TEST(test_three_members_divergent_config_marker);
|
RUN_TEST(test_three_members_divergent_config_marker);
|
||||||
RUN_TEST(test_missing_member_errors);
|
RUN_TEST(test_missing_member_errors);
|
||||||
RUN_TEST(test_too_few_members_errors);
|
RUN_TEST(test_too_few_members_errors);
|
||||||
|
RUN_TEST(test_machine_report_committed_divergence);
|
||||||
|
RUN_TEST(test_report_full_member_table);
|
||||||
|
RUN_TEST(test_report_missing_tagged);
|
||||||
|
RUN_TEST(test_report_uncommitted_tail_classified);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,79 +132,202 @@ static int load_member(const walcompare_member_t* src, member_log_t* m, FILE* ou
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int walcompare(const walcompare_member_t* members, int n, FILE* out,
|
/* Loaded cluster state shared by walcompare() and walcompare_report(). */
|
||||||
walcompare_divergence_t* div)
|
typedef struct {
|
||||||
{
|
|
||||||
if (!members || n < 2)
|
|
||||||
return -EINVAL;
|
|
||||||
if (n > WALCMP_MAX_MEMBERS)
|
|
||||||
return -E2BIG;
|
|
||||||
|
|
||||||
member_log_t logs[WALCMP_MAX_MEMBERS];
|
member_log_t logs[WALCMP_MAX_MEMBERS];
|
||||||
int loaded = 0;
|
int n;
|
||||||
int rc = 0;
|
int loaded; /* members successfully loaded (for cleanup) */
|
||||||
|
uint64_t boundary; /* highest committed LSN compared (committed edge) */
|
||||||
|
uint64_t max_tip; /* highest recovered tip across members (tail edge) */
|
||||||
|
} wc_cluster_t;
|
||||||
|
|
||||||
|
/* Stable status token for one member at one LSN. */
|
||||||
|
static const char* wc_status(const member_log_t* m, uint64_t lsn)
|
||||||
|
{
|
||||||
|
if ((lsn < m->cap) && m->present[lsn])
|
||||||
|
return "present";
|
||||||
|
if (lsn <= m->committed_lsn)
|
||||||
|
return "MISSING"; /* a committed entry this member SHOULD hold (R4.2) */
|
||||||
|
return "beyond"; /* legitimately not held (above its committed prefix) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Load all members and compute the committed boundary + tail edge. On a load
|
||||||
|
* error, frees what loaded, reports to `out`, returns a negative errno. */
|
||||||
|
static int wc_load(const walcompare_member_t* members, int n, FILE* out,
|
||||||
|
wc_cluster_t* cl)
|
||||||
|
{
|
||||||
|
memset(cl, 0, sizeof(*cl));
|
||||||
|
cl->n = n;
|
||||||
|
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
rc = load_member(&members[i], &logs[i], out);
|
int rc = load_member(&members[i], &cl->logs[i], out);
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
if (out)
|
if (out)
|
||||||
fprintf(out, "walcompare: cannot load member %d ('%s'): %s\n", i,
|
fprintf(out, "walcompare: cannot load member %d ('%s'): %s\n", i,
|
||||||
members[i].path ? members[i].path : "(null)",
|
members[i].path ? members[i].path : "(null)",
|
||||||
strerror(-rc));
|
strerror(-rc));
|
||||||
goto done;
|
for (int j = 0; j < cl->loaded; j++)
|
||||||
|
member_log_free(&cl->logs[j]);
|
||||||
|
return rc;
|
||||||
}
|
}
|
||||||
loaded = i + 1;
|
cl->loaded = i + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shared committed boundary = the minimum recovered tip across members:
|
/* Shared committed boundary = the minimum recovered tip across members:
|
||||||
* the largest committed prefix every member can be expected to hold. A
|
* the largest committed prefix every member can be expected to hold. A
|
||||||
* member with an explicit committed_lsn keeps its own. */
|
* member with an explicit committed_lsn keeps its own. */
|
||||||
uint64_t shared = logs[0].recovered_tip;
|
uint64_t shared = cl->logs[0].recovered_tip;
|
||||||
for (int i = 1; i < n; i++)
|
for (int i = 1; i < n; i++)
|
||||||
if (logs[i].recovered_tip < shared)
|
if (cl->logs[i].recovered_tip < shared)
|
||||||
shared = logs[i].recovered_tip;
|
shared = cl->logs[i].recovered_tip;
|
||||||
|
|
||||||
uint64_t boundary = 0; /* highest committed LSN compared across the cluster */
|
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
if (logs[i].committed_lsn == 0)
|
if (cl->logs[i].committed_lsn == 0)
|
||||||
logs[i].committed_lsn = shared;
|
cl->logs[i].committed_lsn = shared;
|
||||||
if (logs[i].committed_lsn > boundary)
|
if (cl->logs[i].committed_lsn > cl->boundary)
|
||||||
boundary = logs[i].committed_lsn;
|
cl->boundary = cl->logs[i].committed_lsn;
|
||||||
|
if (cl->logs[i].recovered_tip > cl->max_tip)
|
||||||
|
cl->max_tip = cl->logs[i].recovered_tip;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wc_unload(wc_cluster_t* cl)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < cl->loaded; i++)
|
||||||
|
member_log_free(&cl->logs[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One machine line for member `i` at `lsn`. Fixed field order, stable tokens. */
|
||||||
|
static void wc_emit_member(const wc_cluster_t* cl, int i, uint64_t lsn,
|
||||||
|
int committed, FILE* out)
|
||||||
|
{
|
||||||
|
const member_log_t* m = &cl->logs[i];
|
||||||
|
int holds = (lsn < m->cap) && m->present[lsn];
|
||||||
|
fprintf(out,
|
||||||
|
"WALCMP member=%d lsn=%" PRIu64 " term=%" PRIu64
|
||||||
|
" pcrc=%08x status=%s committed=%d\n",
|
||||||
|
i, lsn, holds ? match_term(m->term[lsn]) : 0,
|
||||||
|
holds ? m->pcrc[lsn] : 0u, wc_status(m, lsn), committed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full per-member machine table over [1..max_tip]; committed=1 within the
|
||||||
|
* committed boundary, 0 in the uncommitted tail. */
|
||||||
|
static void wc_emit_table(const wc_cluster_t* cl, FILE* out)
|
||||||
|
{
|
||||||
|
for (uint64_t lsn = 1; lsn <= cl->max_tip; lsn++) {
|
||||||
|
int committed = (lsn <= cl->boundary) ? 1 : 0;
|
||||||
|
for (int i = 0; i < cl->n; i++)
|
||||||
|
wc_emit_member(cl, i, lsn, committed, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Emit the classification + per-member identity for ONE diverging LSN. */
|
||||||
|
static void wc_emit_divergence(const wc_cluster_t* cl, uint64_t lsn,
|
||||||
|
int committed, int compared, int present,
|
||||||
|
FILE* out)
|
||||||
|
{
|
||||||
|
fprintf(out,
|
||||||
|
"WALCMP_DIVERGENCE lsn=%" PRIu64 " class=%s compared=%d present=%d\n",
|
||||||
|
lsn, committed ? "committed" : "uncommitted", compared, present);
|
||||||
|
for (int i = 0; i < cl->n; i++)
|
||||||
|
wc_emit_member(cl, i, lsn, committed, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compare one committed LSN. Fills `div` (committed=1) and prints the human +
|
||||||
|
* machine report when divergent. Returns 1 if divergent, else 0. */
|
||||||
|
static int wc_compare_committed(const wc_cluster_t* cl,
|
||||||
|
const walcompare_member_t* members, uint64_t lsn,
|
||||||
|
FILE* out, walcompare_divergence_t* div)
|
||||||
|
{
|
||||||
|
int have = 0, present = 0, have_ref = 0, divergent = 0;
|
||||||
|
uint64_t ref_term = 0;
|
||||||
|
uint32_t ref_pcrc = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < cl->n; i++) {
|
||||||
|
if (lsn > cl->logs[i].committed_lsn)
|
||||||
|
continue; /* beyond member i's committed prefix */
|
||||||
|
have++;
|
||||||
|
const member_log_t* m = &cl->logs[i];
|
||||||
|
int holds = (lsn < m->cap) && m->present[lsn];
|
||||||
|
if (!holds) {
|
||||||
|
divergent = 1; /* member should hold this committed LSN but doesn't */
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
present++;
|
||||||
|
uint64_t mt = match_term(m->term[lsn]);
|
||||||
|
uint32_t mp = m->pcrc[lsn];
|
||||||
|
if (!have_ref) {
|
||||||
|
ref_term = mt;
|
||||||
|
ref_pcrc = mp;
|
||||||
|
have_ref = 1;
|
||||||
|
} else if (mt != ref_term || mp != ref_pcrc) {
|
||||||
|
divergent = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (out)
|
if (have < 2 || !divergent)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (div) {
|
||||||
|
memset(div, 0, sizeof(*div));
|
||||||
|
div->lsn = lsn;
|
||||||
|
div->n_members = cl->n;
|
||||||
|
div->committed = 1;
|
||||||
|
for (int i = 0; i < cl->n && i < 16; i++) {
|
||||||
|
const member_log_t* m = &cl->logs[i];
|
||||||
|
int holds = (lsn <= m->committed_lsn) && (lsn < m->cap) &&
|
||||||
|
m->present[lsn];
|
||||||
|
div->member_present[i] = holds ? 1 : 0;
|
||||||
|
div->member_term[i] = holds ? match_term(m->term[lsn]) : 0;
|
||||||
|
div->member_pcrc[i] = holds ? m->pcrc[lsn] : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (out) {
|
||||||
fprintf(out,
|
fprintf(out,
|
||||||
"== walcompare: %d members, committed boundary up to LSN %" PRIu64
|
"*** DIVERGENCE at committed LSN %" PRIu64
|
||||||
" ==\n",
|
" (compared %d, present %d) ***\n",
|
||||||
n, boundary);
|
lsn, have, present);
|
||||||
|
for (int i = 0; i < cl->n; i++) {
|
||||||
/* Walk every committed LSN. At each LSN, compare the (term, pcrc) of the
|
if (lsn > cl->logs[i].committed_lsn) {
|
||||||
* members whose own committed boundary covers it and that hold the entry.
|
fprintf(out,
|
||||||
* A member that does not hold a committed LSN it SHOULD (boundary covers it
|
" member %d ('%s'): LSN beyond its committed prefix"
|
||||||
* but the entry is absent) is itself a committed-prefix divergence: a
|
" (committed=%" PRIu64 ")\n",
|
||||||
* committed entry is missing (R4.2 / R6.4). */
|
i, members[i].path, cl->logs[i].committed_lsn);
|
||||||
for (uint64_t lsn = 1; lsn <= boundary; lsn++) {
|
|
||||||
int have = 0; /* members whose boundary covers this LSN */
|
|
||||||
int present = 0; /* of those, how many actually hold it */
|
|
||||||
uint64_t ref_term = 0;
|
|
||||||
uint32_t ref_pcrc = 0;
|
|
||||||
int have_ref = 0;
|
|
||||||
int divergent = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
if (lsn > logs[i].committed_lsn)
|
|
||||||
continue; /* this LSN is beyond member i's committed prefix */
|
|
||||||
have++;
|
|
||||||
|
|
||||||
int holds = (lsn < logs[i].cap) && logs[i].present[lsn];
|
|
||||||
if (!holds) {
|
|
||||||
/* member i should hold this committed LSN but does not */
|
|
||||||
divergent = 1;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
int holds = (lsn < cl->logs[i].cap) && cl->logs[i].present[lsn];
|
||||||
|
if (holds)
|
||||||
|
fprintf(out, " member %d ('%s'): term=%" PRIu64 " pcrc=%08x\n",
|
||||||
|
i, members[i].path, match_term(cl->logs[i].term[lsn]),
|
||||||
|
cl->logs[i].pcrc[lsn]);
|
||||||
|
else
|
||||||
|
fprintf(out, " member %d ('%s'): MISSING committed entry\n", i,
|
||||||
|
members[i].path);
|
||||||
|
}
|
||||||
|
wc_emit_divergence(cl, lsn, 1, have, present, out);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scan the uncommitted tail (boundary < lsn <= max_tip): emit a NON-fatal
|
||||||
|
* class=uncommitted classification for any LSN where >=2 members hold the entry
|
||||||
|
* but disagree on (term, pcrc). Never changes the verdict. */
|
||||||
|
static void wc_scan_uncommitted(const wc_cluster_t* cl, FILE* out)
|
||||||
|
{
|
||||||
|
for (uint64_t lsn = cl->boundary + 1; lsn <= cl->max_tip; lsn++) {
|
||||||
|
int present = 0, have_ref = 0, divergent = 0;
|
||||||
|
uint64_t ref_term = 0;
|
||||||
|
uint32_t ref_pcrc = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < cl->n; i++) {
|
||||||
|
const member_log_t* m = &cl->logs[i];
|
||||||
|
int holds = (lsn < m->cap) && m->present[lsn];
|
||||||
|
if (!holds)
|
||||||
|
continue;
|
||||||
present++;
|
present++;
|
||||||
uint64_t mt = match_term(logs[i].term[lsn]);
|
uint64_t mt = match_term(m->term[lsn]);
|
||||||
uint32_t mp = logs[i].pcrc[lsn];
|
uint32_t mp = m->pcrc[lsn];
|
||||||
if (!have_ref) {
|
if (!have_ref) {
|
||||||
ref_term = mt;
|
ref_term = mt;
|
||||||
ref_pcrc = mp;
|
ref_pcrc = mp;
|
||||||
|
|
@ -213,60 +336,69 @@ int walcompare(const walcompare_member_t* members, int n, FILE* out,
|
||||||
divergent = 1;
|
divergent = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (present < 2 || !divergent)
|
||||||
if (have < 2)
|
|
||||||
continue; /* nothing to compare at this LSN */
|
|
||||||
if (!divergent)
|
|
||||||
continue;
|
continue;
|
||||||
|
wc_emit_divergence(cl, lsn, 0, present, present, out);
|
||||||
/* First divergent committed LSN — report and fail. */
|
|
||||||
if (div) {
|
|
||||||
memset(div, 0, sizeof(*div));
|
|
||||||
div->lsn = lsn;
|
|
||||||
div->n_members = n;
|
|
||||||
for (int i = 0; i < n && i < 16; i++) {
|
|
||||||
int holds = (lsn <= logs[i].committed_lsn) &&
|
|
||||||
(lsn < logs[i].cap) && logs[i].present[lsn];
|
|
||||||
div->member_present[i] = holds ? 1 : 0;
|
|
||||||
div->member_term[i] = holds ? match_term(logs[i].term[lsn]) : 0;
|
|
||||||
div->member_pcrc[i] = holds ? logs[i].pcrc[lsn] : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (out) {
|
|
||||||
fprintf(out,
|
|
||||||
"*** DIVERGENCE at committed LSN %" PRIu64
|
|
||||||
" (compared %d, present %d) ***\n",
|
|
||||||
lsn, have, present);
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
if (lsn > logs[i].committed_lsn) {
|
|
||||||
fprintf(out,
|
|
||||||
" member %d ('%s'): LSN beyond its committed prefix"
|
|
||||||
" (committed=%" PRIu64 ")\n",
|
|
||||||
i, members[i].path, logs[i].committed_lsn);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int holds = (lsn < logs[i].cap) && logs[i].present[lsn];
|
|
||||||
if (holds)
|
|
||||||
fprintf(out,
|
|
||||||
" member %d ('%s'): term=%" PRIu64 " pcrc=%08x\n", i,
|
|
||||||
members[i].path, match_term(logs[i].term[lsn]),
|
|
||||||
logs[i].pcrc[lsn]);
|
|
||||||
else
|
|
||||||
fprintf(out,
|
|
||||||
" member %d ('%s'): MISSING committed entry\n", i,
|
|
||||||
members[i].path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rc = WALCMP_DIVERGENT;
|
|
||||||
goto done;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shared driver for walcompare() (full_report=0) and walcompare_report()
|
||||||
|
* (full_report=1). */
|
||||||
|
static int wc_run(const walcompare_member_t* members, int n, FILE* out,
|
||||||
|
walcompare_divergence_t* div, int full_report)
|
||||||
|
{
|
||||||
|
if (!members || n < 2)
|
||||||
|
return -EINVAL;
|
||||||
|
if (n > WALCMP_MAX_MEMBERS)
|
||||||
|
return -E2BIG;
|
||||||
|
|
||||||
|
wc_cluster_t cl;
|
||||||
|
int rc = wc_load(members, n, out, &cl);
|
||||||
|
if (rc != 0)
|
||||||
|
return rc;
|
||||||
|
|
||||||
|
if (out)
|
||||||
|
fprintf(out,
|
||||||
|
"== walcompare: %d members, committed boundary up to LSN %" PRIu64
|
||||||
|
" ==\n",
|
||||||
|
n, cl.boundary);
|
||||||
|
|
||||||
|
if (full_report && out)
|
||||||
|
wc_emit_table(&cl, out);
|
||||||
|
|
||||||
|
/* Walk every committed LSN. At each LSN, compare the (term, pcrc) of the
|
||||||
|
* members whose own committed boundary covers it and that hold the entry.
|
||||||
|
* A member that does not hold a committed LSN it SHOULD is a committed-
|
||||||
|
* prefix divergence: a committed entry is missing (R4.2 / R6.4). */
|
||||||
|
rc = WALCMP_OK;
|
||||||
|
for (uint64_t lsn = 1; lsn <= cl.boundary; lsn++) {
|
||||||
|
if (wc_compare_committed(&cl, members, lsn, out, div)) {
|
||||||
|
rc = WALCMP_DIVERGENT; /* first committed divergence is the verdict */
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The committed prefix is identical. In full-report mode also classify any
|
||||||
|
* uncommitted-tail divergence (informational, never fatal). */
|
||||||
|
if (full_report && out)
|
||||||
|
wc_scan_uncommitted(&cl, out);
|
||||||
|
|
||||||
if (out)
|
if (out)
|
||||||
fprintf(out, "walcompare: OK — committed prefixes identical\n");
|
fprintf(out, "walcompare: OK — committed prefixes identical\n");
|
||||||
rc = WALCMP_OK;
|
|
||||||
|
|
||||||
done:
|
done:
|
||||||
for (int i = 0; i < loaded; i++)
|
wc_unload(&cl);
|
||||||
member_log_free(&logs[i]);
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int walcompare(const walcompare_member_t* members, int n, FILE* out,
|
||||||
|
walcompare_divergence_t* div)
|
||||||
|
{
|
||||||
|
return wc_run(members, n, out, div, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int walcompare_report(const walcompare_member_t* members, int n, FILE* out,
|
||||||
|
walcompare_divergence_t* div)
|
||||||
|
{
|
||||||
|
return wc_run(members, n, out, div, 1);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ typedef struct {
|
||||||
uint64_t member_term[16]; /* per-member matched term at lsn */
|
uint64_t member_term[16]; /* per-member matched term at lsn */
|
||||||
uint32_t member_pcrc[16]; /* per-member payload CRC at lsn */
|
uint32_t member_pcrc[16]; /* per-member payload CRC at lsn */
|
||||||
int member_present[16]; /* 1 if the member holds this LSN, else 0 */
|
int member_present[16]; /* 1 if the member holds this LSN, else 0 */
|
||||||
|
int committed; /* 1 = divergence within the committed prefix
|
||||||
|
* (lsn <= committed boundary, a safety
|
||||||
|
* violation); 0 = uncommitted tail divergence */
|
||||||
} walcompare_divergence_t;
|
} walcompare_divergence_t;
|
||||||
|
|
||||||
/* Result codes. */
|
/* Result codes. */
|
||||||
|
|
@ -62,3 +65,29 @@ enum {
|
||||||
*/
|
*/
|
||||||
int walcompare(const walcompare_member_t* members, int n, FILE* out,
|
int walcompare(const walcompare_member_t* members, int n, FILE* out,
|
||||||
walcompare_divergence_t* div);
|
walcompare_divergence_t* div);
|
||||||
|
|
||||||
|
/* Same comparison as walcompare(), but ALSO emits a machine-checkable,
|
||||||
|
* per-member report over the full log: one
|
||||||
|
*
|
||||||
|
* WALCMP member=<i> lsn=<L> term=<T> pcrc=<8hex> status=<present|MISSING|beyond> committed=<0|1>
|
||||||
|
*
|
||||||
|
* line per (member, LSN) for every LSN in [1..max-recovered-tip], where
|
||||||
|
* `committed=1` iff the LSN is within the committed boundary. Each divergence
|
||||||
|
* additionally emits a
|
||||||
|
*
|
||||||
|
* WALCMP_DIVERGENCE lsn=<L> class=<committed|uncommitted> compared=<c> present=<p>
|
||||||
|
*
|
||||||
|
* classification line. A `committed` divergence is fatal (verdict
|
||||||
|
* WALCMP_DIVERGENT, `div->committed = 1`); an `uncommitted`-tail divergence
|
||||||
|
* (above the committed boundary) is reported but NON-fatal — the verdict stays
|
||||||
|
* WALCMP_OK when the committed prefix is identical. Field order is fixed and
|
||||||
|
* the tokens are stable so the harness can grep them. `out` must be non-NULL.
|
||||||
|
*
|
||||||
|
* Backward-compatible: walcompare() keeps its original concise output (the
|
||||||
|
* `== walcompare ==` header, the human divergence block, and the final
|
||||||
|
* `walcompare: OK` verdict line) and additionally emits the machine
|
||||||
|
* classification at a committed divergence; only walcompare_report() prints the
|
||||||
|
* full per-LSN table and scans the uncommitted tail.
|
||||||
|
*/
|
||||||
|
int walcompare_report(const walcompare_member_t* members, int n, FILE* out,
|
||||||
|
walcompare_divergence_t* div);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue