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:
Rongsong Shen 2026-06-20 09:17:07 +00:00
parent 65ddafeb66
commit 3799bd6af4
3 changed files with 183 additions and 8 deletions

View file

@ -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);