forked from lei.chen/myxce
feat(controller-status): off-primary learned committed + responder timestamp in ctrlstatus_fill (task 6.2)
Role-branch the committed sourcing: PRIMARY reports its own authoritative persist.committed_lsn / committed_config_tag; off-primary reports the LEARNED ha->last_known_committed_lsn / last_known_committed_config_tag instead of the primary-only raw markers that read 0 on a standby (fixes the applied>committed inversion). Stamp responder_time_ms from CLOCK_REALTIME for log correlation. Read-only (Req 10.1-10.4, 11.1). 4 new TDD tests; controller suite 110/0/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
65ddafeb66
commit
3799bd6af4
3 changed files with 183 additions and 8 deletions
|
|
@ -17,6 +17,7 @@
|
|||
#include <stdatomic.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "cluster.h"
|
||||
#include "ha.h"
|
||||
|
|
@ -104,10 +105,18 @@ void ctrlstatus_fill(cluster_t* c, xce_ctrlstatus_t* st)
|
|||
st->primaryName = strdup(""); /* unknown — not reliably known off-primary */
|
||||
}
|
||||
|
||||
/* loaded config version (+ committed version) */
|
||||
st->configVersion = mc ? mc->config_version_tag : 0;
|
||||
st->committedConfigVersion = atomic_load_explicit(&ha->committed_config_tag,
|
||||
memory_order_acquire);
|
||||
/* responder snapshot timestamp (Req 11.1): CLOCK_REALTIME (wall clock, NOT
|
||||
* CLOCK_MONOTONIC which is used for heartbeat liveness) at the sample
|
||||
* moment, in epoch millis, so an operator can line a captured status up
|
||||
* against this responder's log timestamps. Stamped here with the other
|
||||
* field loads to mark the snapshot instant. */
|
||||
struct timespec ctrlstatus_ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ctrlstatus_ts);
|
||||
st->responder_time_ms = (uint64_t)ctrlstatus_ts.tv_sec * 1000u
|
||||
+ (uint64_t)ctrlstatus_ts.tv_nsec / 1000000u;
|
||||
|
||||
/* loaded config version */
|
||||
st->configVersion = mc ? mc->config_version_tag : 0;
|
||||
|
||||
/* configChecksum: 64-char lowercase-hex of the loaded config_sha256[32]
|
||||
* (Req 5.2 content checksum, 5.3 divergence detection — see NOTE above).
|
||||
|
|
@ -126,9 +135,29 @@ void ctrlstatus_fill(cluster_t* c, xce_ctrlstatus_t* st)
|
|||
}
|
||||
}
|
||||
|
||||
/* commit / apply progress (Req 6.2) */
|
||||
st->committedLsn = atomic_load_explicit(&c->persist.committed_lsn,
|
||||
memory_order_acquire);
|
||||
/* commit / apply progress (Req 6.2) — committed sourcing is role-branched
|
||||
* (Req 10.1/10.2): a PRIMARY's own live markers are authoritative; any
|
||||
* other role (STANDBY/ELECTING/INIT) has never served as primary, so its
|
||||
* node-local committed marker is unset (0) and the only honest "committed"
|
||||
* knowledge is what it has LEARNED from the primary's heartbeats. Reporting
|
||||
* the learned pair avoids a misleading committed=0 while the node has
|
||||
* clearly applied replicated entries, and keeps committed/applied sourced
|
||||
* consistently (no spurious applied>committed inversion, Req 10.3). Pre-
|
||||
* learn the learned values are 0 → well-defined "unknown" (Req 10.2). */
|
||||
if (st->responderRole == (uint8_t)CTRL_PRIMARY) {
|
||||
st->committedLsn = atomic_load_explicit(&c->persist.committed_lsn,
|
||||
memory_order_acquire);
|
||||
st->committedConfigVersion =
|
||||
atomic_load_explicit(&ha->committed_config_tag,
|
||||
memory_order_acquire);
|
||||
} else {
|
||||
st->committedLsn = atomic_load_explicit(&ha->last_known_committed_lsn,
|
||||
memory_order_acquire);
|
||||
/* last_known_committed_config_tag is a plain (non-atomic) uint64_t
|
||||
* written under ha->lock; a best-effort aligned 8-byte read is correct
|
||||
* here and must NOT take the lock on this read path. */
|
||||
st->committedConfigVersion = ha->last_known_committed_config_tag;
|
||||
}
|
||||
st->appliedLsn = atomic_load_explicit(&ha->applied_lsn,
|
||||
memory_order_acquire);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
|
|
@ -332,6 +333,147 @@ static void test_ctrlstatus_no_mutation(void)
|
|||
printf("test_ctrlstatus_no_mutation: PASS\n");
|
||||
}
|
||||
|
||||
/* (f) STANDBY reports the LEARNED committed position (Req 10.1, 10.3).
|
||||
* A standby has never served as primary, so its node-local committed marker
|
||||
* (persist.committed_lsn) is 0, but it has learned a committed position from
|
||||
* the primary's heartbeats (last_known_committed_lsn) and has applied entries
|
||||
* beyond that learned floor. The reply must report the LEARNED committed LSN +
|
||||
* committed config version (not the node-local zero) and the real applied
|
||||
* position — with no applied>committed inversion relative to what it knows. */
|
||||
static void test_ctrlstatus_standby_reports_learned_committed(void)
|
||||
{
|
||||
cluster_t c;
|
||||
myconf_t mc;
|
||||
cs_setup_cluster(&c, &mc);
|
||||
|
||||
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY,
|
||||
memory_order_release);
|
||||
/* node-local committed marker is unset on a never-primary node */
|
||||
atomic_store_explicit(&c.persist.committed_lsn, 0, memory_order_release);
|
||||
/* the primary's own committed_config_tag is also unset locally */
|
||||
atomic_store_explicit(&c.ha.committed_config_tag, 0, memory_order_release);
|
||||
/* applied beyond the learned committed floor */
|
||||
atomic_store_explicit(&c.ha.applied_lsn, 38, memory_order_release);
|
||||
/* learned from the primary's heartbeats */
|
||||
atomic_store_explicit(&c.ha.last_known_committed_lsn, 36,
|
||||
memory_order_release);
|
||||
c.ha.last_known_committed_config_tag = 0x00C0FFEE0000CAFEULL;
|
||||
|
||||
xce_ctrlstatus_t st;
|
||||
ctrlstatus_fill(&c, &st);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_STANDBY, st.responderRole);
|
||||
/* Req 10.1: learned committed values, not the node-local zero */
|
||||
TEST_ASSERT_EQUAL_UINT64(36, st.committedLsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(0x00C0FFEE0000CAFEULL, st.committedConfigVersion);
|
||||
/* Req 10.3: real applied position; no applied>committed inversion vs the
|
||||
* node's own knowledge (applied 38 >= learned committed 36 is consistent). */
|
||||
TEST_ASSERT_EQUAL_UINT64(38, st.appliedLsn);
|
||||
|
||||
ctrlstatus_free_fields(&st);
|
||||
cs_teardown_cluster(&c);
|
||||
printf("test_ctrlstatus_standby_reports_learned_committed: PASS\n");
|
||||
}
|
||||
|
||||
/* (g) STANDBY pre-learn → committed LSN + committed config version are the
|
||||
* well-defined "unknown"/zero value (Req 10.2). The node has not yet learned
|
||||
* any committed position from a primary, so it must report 0 (not a stale or
|
||||
* guessed value), consistent with Req 1.3. */
|
||||
static void test_ctrlstatus_standby_prelearn_committed_zero(void)
|
||||
{
|
||||
cluster_t c;
|
||||
myconf_t mc;
|
||||
cs_setup_cluster(&c, &mc);
|
||||
|
||||
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY,
|
||||
memory_order_release);
|
||||
atomic_store_explicit(&c.ha.last_known_committed_lsn, 0,
|
||||
memory_order_release);
|
||||
c.ha.last_known_committed_config_tag = 0;
|
||||
/* even if the node-local primary marker had a stale value, off-primary it
|
||||
* must NOT be surfaced — prove the learned (zero) value wins. */
|
||||
atomic_store_explicit(&c.persist.committed_lsn, 77, memory_order_release);
|
||||
atomic_store_explicit(&c.ha.committed_config_tag, 0xDEADULL,
|
||||
memory_order_release);
|
||||
|
||||
xce_ctrlstatus_t st;
|
||||
ctrlstatus_fill(&c, &st);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_STANDBY, st.responderRole);
|
||||
TEST_ASSERT_EQUAL_UINT64(0, st.committedLsn); /* Req 10.2 */
|
||||
TEST_ASSERT_EQUAL_UINT64(0, st.committedConfigVersion); /* Req 10.2 */
|
||||
|
||||
ctrlstatus_free_fields(&st);
|
||||
cs_teardown_cluster(&c);
|
||||
printf("test_ctrlstatus_standby_prelearn_committed_zero: PASS\n");
|
||||
}
|
||||
|
||||
/* (h) PRIMARY reports its OWN authoritative local committed markers
|
||||
* (persist.committed_lsn, ha->committed_config_tag) — unchanged behavior. The
|
||||
* learned last_known_* values must NOT shadow the primary's own markers. */
|
||||
static void test_ctrlstatus_primary_reports_own_committed(void)
|
||||
{
|
||||
cluster_t c;
|
||||
myconf_t mc;
|
||||
cs_setup_cluster(&c, &mc);
|
||||
|
||||
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_PRIMARY,
|
||||
memory_order_release);
|
||||
atomic_store_explicit(&c.persist.committed_lsn, 512, memory_order_release);
|
||||
atomic_store_explicit(&c.ha.committed_config_tag, 0x0BADF00DULL,
|
||||
memory_order_release);
|
||||
/* stale learned values must NOT be surfaced on a primary */
|
||||
atomic_store_explicit(&c.ha.last_known_committed_lsn, 9,
|
||||
memory_order_release);
|
||||
c.ha.last_known_committed_config_tag = 0x1111ULL;
|
||||
|
||||
xce_ctrlstatus_t st;
|
||||
ctrlstatus_fill(&c, &st);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_PRIMARY, st.responderRole);
|
||||
TEST_ASSERT_EQUAL_UINT64(512, st.committedLsn); /* own */
|
||||
TEST_ASSERT_EQUAL_UINT64(0x0BADF00DULL, st.committedConfigVersion); /* own */
|
||||
|
||||
ctrlstatus_free_fields(&st);
|
||||
cs_teardown_cluster(&c);
|
||||
printf("test_ctrlstatus_primary_reports_own_committed: PASS\n");
|
||||
}
|
||||
|
||||
/* (i) Responder snapshot timestamp (Req 11.1): responder_time_ms is the
|
||||
* responder's CLOCK_REALTIME wall clock (epoch ms) at sample time — non-zero
|
||||
* and within [before, after] of a test-captured wall-clock sample. */
|
||||
static void test_ctrlstatus_responder_time_ms(void)
|
||||
{
|
||||
cluster_t c;
|
||||
myconf_t mc;
|
||||
cs_setup_cluster(&c, &mc);
|
||||
|
||||
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY,
|
||||
memory_order_release);
|
||||
|
||||
struct timespec before_ts;
|
||||
clock_gettime(CLOCK_REALTIME, &before_ts);
|
||||
uint64_t before_ms = (uint64_t)before_ts.tv_sec * 1000u
|
||||
+ (uint64_t)before_ts.tv_nsec / 1000000u;
|
||||
|
||||
xce_ctrlstatus_t st;
|
||||
ctrlstatus_fill(&c, &st);
|
||||
|
||||
struct timespec after_ts;
|
||||
clock_gettime(CLOCK_REALTIME, &after_ts);
|
||||
uint64_t after_ms = (uint64_t)after_ts.tv_sec * 1000u
|
||||
+ (uint64_t)after_ts.tv_nsec / 1000000u;
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL_UINT64(0, st.responder_time_ms);
|
||||
/* within the captured window (1 ms slack each side for clock resolution) */
|
||||
TEST_ASSERT_TRUE(st.responder_time_ms + 1u >= before_ms);
|
||||
TEST_ASSERT_TRUE(st.responder_time_ms <= after_ms + 1u);
|
||||
|
||||
ctrlstatus_free_fields(&st);
|
||||
cs_teardown_cluster(&c);
|
||||
printf("test_ctrlstatus_responder_time_ms: PASS\n");
|
||||
}
|
||||
|
||||
/* ── suite entry point ─────────────────────────────────────── */
|
||||
|
||||
void test_ctrlstatus(void)
|
||||
|
|
@ -343,4 +485,8 @@ void test_ctrlstatus(void)
|
|||
RUN_TEST(test_ctrlstatus_electing_is_unknown);
|
||||
RUN_TEST(test_ctrlstatus_fenced_when_in_catch_up);
|
||||
RUN_TEST(test_ctrlstatus_no_mutation);
|
||||
RUN_TEST(test_ctrlstatus_standby_reports_learned_committed);
|
||||
RUN_TEST(test_ctrlstatus_standby_prelearn_committed_zero);
|
||||
RUN_TEST(test_ctrlstatus_primary_reports_own_committed);
|
||||
RUN_TEST(test_ctrlstatus_responder_time_ms);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue