From 93f32b5d8407c6962fc895a2959c76d7285668c5 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 16 Sep 2026 07:21:36 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC --- .kiro/specs/controller-status/tasks.md | 2 +- src/controller/ctrlstatus.c | 14 +++++ src/controller/testsuite/test_ctrlstatus.c | 72 ++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/.kiro/specs/controller-status/tasks.md b/.kiro/specs/controller-status/tasks.md index 119e0cee..dbfc22fe 100644 --- a/.kiro/specs/controller-status/tasks.md +++ b/.kiro/specs/controller-status/tasks.md @@ -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. diff --git a/src/controller/ctrlstatus.c b/src/controller/ctrlstatus.c index cd17530b..5ec128bb 100644 --- a/src/controller/ctrlstatus.c +++ b/src/controller/ctrlstatus.c @@ -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) diff --git a/src/controller/testsuite/test_ctrlstatus.c b/src/controller/testsuite/test_ctrlstatus.c index 6c02f084..e1478715 100644 --- a/src/controller/testsuite/test_ctrlstatus.c +++ b/src/controller/testsuite/test_ctrlstatus.c @@ -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); }