feat(controller-status): source election time in ctrlstatus_fill (task 7.4)

ctrlstatus_fill now unconditionally reads ha->last_known_elected_at_ms
into the reply's elected_at_ms field -- one atomic-acquire load, no
role-branch, mirroring how the handler already reports raw term
unconditionally. Serves both a self-stamped primary and a
heartbeat-learned standby from the same field. "Unknown" display
gating is deliberately left to the client command (task 7.5), not
this handler.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
Rongsong Shen 2026-09-16 07:21:36 +00:00
parent 30adc52ea3
commit 93f32b5d84
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
3 changed files with 87 additions and 1 deletions

View file

@ -116,7 +116,7 @@
- _Boundary: heartbeat (send + receive)_
- _Depends: 7.1_
- [ ] 7.4 Source the election time in the status handler
- [x] 7.4 Source the election time in the status handler
- TDD (red first): the status handler reports a non-zero election-time equal to the responder's own learned value (self-stamped on a primary, heartbeat-learned on a standby); a standby seeded with a learned election-time (simulating an already-processed heartbeat) reports that learned value, not zero; the handler still mutates no state.
- Read the learned election-time into the status reply unconditionally (no role-based branching, no "unknown" gating in the handler itself — that display decision belongs to the client command in 7.5, mirroring how the handler already reports raw term unconditionally regardless of whether a primary is currently known).
- Observable: the new unit tests go red → green; a standby seeded with a learned election-time reports that value instead of zero; the no-mutation assertion holds.

View file

@ -90,6 +90,20 @@ void ctrlstatus_fill(cluster_t* c, xce_ctrlstatus_t* st)
memory_order_acquire);
st->term = atomic_load_explicit(&ha->term, memory_order_acquire);
/* current primary's election time (Req 12.1, 12.2, 12.5): a single
* unconditional read of last_known_elected_at_ms -- no role-branch. Both
* promotion roads self-stamp this field at the instant they commit to
* CTRL_PRIMARY (task 7.2), and it is unconditionally learned off-primary
* via heartbeat (task 7.3), so one atomic load correctly serves both the
* primary (self-stamped) and standby (heartbeat-learned) case -- unlike
* committedLsn/committedConfigVersion above, there is no separate
* primary-only marker to branch on here. "Unknown" display gating (Req
* 12.3) is a client-side print-layer decision (task 7.5), not this
* handler's concern -- mirrors how st->term above is also read
* unconditionally regardless of whether a primary is currently known. */
st->elected_at_ms = atomic_load_explicit(&ha->last_known_elected_at_ms,
memory_order_acquire);
/* primary identity (Req 2.1/2.2/2.3). Only the PRIMARY can authoritatively
* name the primary — itself. The controller does NOT track the current
* primary's NAME on a standby: ha->primary_host is vestigial (never written)

View file

@ -298,6 +298,9 @@ static void test_ctrlstatus_no_mutation(void)
memory_order_release);
atomic_store_explicit(&c.ha.in_catch_up, 0, memory_order_release);
strncpy(c.ha.voted_for_name, "ctrl-1", sizeof(c.ha.voted_for_name) - 1);
/* Req 12.5: reading the learned election-time must not mutate it either. */
atomic_store_explicit(&c.ha.last_known_elected_at_ms,
UINT64_C(1781939830911), memory_order_release);
int32_t role0 = atomic_load_explicit(&c.ha.role, memory_order_acquire);
uint64_t term0 = atomic_load_explicit(&c.ha.term, memory_order_acquire);
@ -312,6 +315,8 @@ static void test_ctrlstatus_no_mutation(void)
char voted0[XCE_MAXNAME_LEN];
strncpy(voted0, c.ha.voted_for_name, sizeof(voted0));
uint64_t vertag0 = mc.config_version_tag;
uint64_t elected0 = atomic_load_explicit(&c.ha.last_known_elected_at_ms,
memory_order_acquire);
xce_ctrlstatus_t st;
ctrlstatus_fill(&c, &st);
@ -335,6 +340,9 @@ static void test_ctrlstatus_no_mutation(void)
memory_order_acquire));
TEST_ASSERT_EQUAL_STRING(voted0, c.ha.voted_for_name);
TEST_ASSERT_EQUAL_UINT64(vertag0, mc.config_version_tag);
TEST_ASSERT_EQUAL_UINT64(elected0,
atomic_load_explicit(&c.ha.last_known_elected_at_ms,
memory_order_acquire));
cs_teardown_cluster(&c);
printf("test_ctrlstatus_no_mutation: PASS\n");
@ -633,6 +641,68 @@ static void test_heartbeatmsg_electedatms_roundtrip(void)
printf("test_heartbeatmsg_electedatms_roundtrip: PASS\n");
}
/* (m) PRIMARY: elected_at_ms reports the responder's own learned value
* (Req 12.1, 12.2, 12.5). Design Delta Req 12 locked decision 2: BOTH
* promotion roads self-stamp `ha->last_known_elected_at_ms` at the instant
* they commit to CTRL_PRIMARY -- there is no separate primary-only marker
* the way there is for committedLsn (Req 10) -- so ctrlstatus_fill needs a
* single UNCONDITIONAL read of last_known_elected_at_ms for every role. This
* test seeds that field directly (same convention as the pre-existing
* committed-position tests) to prove the primary case round-trips it. */
static void test_ctrlstatus_primary_reports_own_elected_at_ms(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);
/* self-stamped by the promotion road (task 7.2); seeded directly here per
* this task's own testing convention. */
atomic_store_explicit(&c.ha.last_known_elected_at_ms,
UINT64_C(1781939830911), memory_order_release);
xce_ctrlstatus_t st;
ctrlstatus_fill(&c, &st);
TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_PRIMARY, st.responderRole);
TEST_ASSERT_NOT_EQUAL_UINT64(0, st.elected_at_ms);
TEST_ASSERT_EQUAL_UINT64(UINT64_C(1781939830911), st.elected_at_ms);
ctrlstatus_free_fields(&st);
cs_teardown_cluster(&c);
printf("test_ctrlstatus_primary_reports_own_elected_at_ms: PASS\n");
}
/* (n) STANDBY seeded with a learned election-time (simulating an already-
* processed heartbeat, task 7.3) reports that LEARNED value, not zero (Req
* 12.1, 12.2). Mirrors test_ctrlstatus_standby_reports_learned_committed's
* pattern for the committed pair -- except here there is no role-branch: the
* handler reads the SAME field unconditionally regardless of responder role. */
static void test_ctrlstatus_standby_reports_learned_elected_at_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);
/* learned from the primary's heartbeat (task 7.3); seeded directly here. */
atomic_store_explicit(&c.ha.last_known_elected_at_ms,
UINT64_C(1781939830911), memory_order_release);
xce_ctrlstatus_t st;
ctrlstatus_fill(&c, &st);
TEST_ASSERT_EQUAL_UINT8((uint8_t)CTRL_STANDBY, st.responderRole);
TEST_ASSERT_NOT_EQUAL_UINT64(0, st.elected_at_ms);
TEST_ASSERT_EQUAL_UINT64(UINT64_C(1781939830911), st.elected_at_ms);
ctrlstatus_free_fields(&st);
cs_teardown_cluster(&c);
printf("test_ctrlstatus_standby_reports_learned_elected_at_ms: PASS\n");
}
/* ── suite entry point ─────────────────────────────────────── */
void test_ctrlstatus(void)
@ -652,4 +722,6 @@ void test_ctrlstatus(void)
RUN_TEST(test_ctrlstatus_responder_time_ms);
RUN_TEST(test_ctrlstatus_electedatms_roundtrip);
RUN_TEST(test_heartbeatmsg_electedatms_roundtrip);
RUN_TEST(test_ctrlstatus_primary_reports_own_elected_at_ms);
RUN_TEST(test_ctrlstatus_standby_reports_learned_elected_at_ms);
}