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

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