feat(controller-status): add election-time schema fields + ha_state_t (task 7.1)
Adds CtrlStatus.electedAtMs@14 and HeartbeatMsg.electedAtMs@4 to the capnp schema, their hand-authored typemap.h counterparts, and a new ha_state_t.last_known_elected_at_ms field alongside last_known_committed_lsn. Schema/state plumbing only -- nothing stamps or consumes these fields yet (tasks 7.2-7.5). Codec round-trip tests confirm both new fields survive encode/decode unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
parent
7556856e5c
commit
cae1039ed7
6 changed files with 184 additions and 1 deletions
|
|
@ -92,7 +92,7 @@
|
|||
## 7. Delta: primary election term + time (Req 12)
|
||||
> Sequential — the election-time stamp touches the same HA promotion/heartbeat control flow as tasks 2.2/6.2 (ha.c); no `(P)`. Read-only reporting only: this task must not change how elections are conducted, how term advances, or how/when role transitions to primary — every write here is an additional observational stamp at an already-decided instant. `electedTerm` needs no new field (design.md Locked Decision 1); it is a CLI-side derivation added in 7.5, not new state.
|
||||
|
||||
- [ ] 7.1 Add the election-time schema fields and controller state
|
||||
- [x] 7.1 Add the election-time schema fields and controller state
|
||||
- Add a wall-clock election-time field to the status reply payload (next free ordinal, append-only) and to the heartbeat message the primary already sends to standbys (next free ordinal).
|
||||
- Add the corresponding piece of HA state that holds the currently-known primary's election time, alongside the existing learned-committed-position state.
|
||||
- Add the matching fields to the hand-authored C structs backing these two payloads (this schema's `$C.mapname` types are NOT auto-generated — the generated codec references hand-written typedefs in `typemap.h`, per tasks 1.1/2.1/6.1's established convention) and verify each new field field-for-field against the regenerated codec.
|
||||
|
|
|
|||
|
|
@ -184,6 +184,17 @@ typedef struct {
|
|||
sem_t heartbeat_seen; /* Req 1: posted by heartbeat handler during CTRL_INIT */
|
||||
_Atomic uint64_t last_known_committed_config_tag; /* Req 8: from primary heartbeats */
|
||||
_Atomic uint64_t committed_config_tag; /* Req 8.1: set on quorum ACK */
|
||||
/* ── New field added by controller-status spec delta (Req 12, task 7.1) ── */
|
||||
_Atomic uint64_t last_known_elected_at_ms; /* Currently-known primary's
|
||||
* election time (epoch ms): self-stamped
|
||||
* by both promotion roads at the instant
|
||||
* each commits to CTRL_PRIMARY (task 7.2,
|
||||
* not yet wired), unconditionally learned
|
||||
* off-primary via heartbeat (task 7.3, not
|
||||
* yet wired) -- mirrors last_known_committed_lsn/
|
||||
* last_known_committed_config_tag above.
|
||||
* 0/unset at init (ha_init memset) until
|
||||
* the first promotion or heartbeat. */
|
||||
/* ── New fields added by election-iomgr-refactor spec (Task 2.1) ── */
|
||||
void *client_ssl_ctx; /* SSL_CTX* (TLS_client_method); created in myapp_run */
|
||||
/* ── New field added by raft-drive-async spec (Task 3.1) ──────── */
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@
|
|||
|
||||
#include "xce/ctrlstatus_fmt.h" /* ctrlstatus_format_time_ms (task 6.3) */
|
||||
|
||||
#include "controller.capnp.h" /* generated codec: CtrlStatus/HeartbeatMsg
|
||||
* encode/decode (task 7.1 round-trip tests) */
|
||||
|
||||
/* ── helpers ────────────────────────────────────────────────── */
|
||||
|
||||
static ha_config_t cs_valid_cfg(void)
|
||||
|
|
@ -511,6 +514,125 @@ static void test_ctrlstatus_format_time_ms(void)
|
|||
printf("test_ctrlstatus_format_time_ms: PASS\n");
|
||||
}
|
||||
|
||||
/* ── (k) codec round-trip: CtrlStatus.electedAtMs @14 (task 7.1, Req 12.1) ──
|
||||
* Design delta "Design Delta — Requirement 12", Schema delta subsection: a new
|
||||
* append-only UInt64 epoch-millis field on CtrlStatus, next free ordinal after
|
||||
* responderTimeMs @13. This is schema/codec-level ONLY: ctrlstatus_fill does
|
||||
* not populate elected_at_ms yet (that wiring is task 7.4, out of boundary for
|
||||
* 7.1) -- the struct is built directly to prove the new wire field survives a
|
||||
* genuine encode -> bytes -> decode cycle unchanged, mirroring the pattern in
|
||||
* src/lib/testsuite/test_ctrlstatus.c's test_ctrlstatus_roundtrip_all_fields.
|
||||
*/
|
||||
static void test_ctrlstatus_electedatms_roundtrip(void)
|
||||
{
|
||||
xce_ctrlstatus_t in;
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.clusterName = "lab-cluster";
|
||||
in.responderName = "ctrl-a";
|
||||
in.elected_at_ms = UINT64_C(1781939830911); /* known non-zero value */
|
||||
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
struct CtrlStatus cap_in;
|
||||
CtrlStatus_ptr sp;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
|
||||
memset(&cap_in, 0, sizeof(cap_in));
|
||||
encode_CtrlStatus(cs, &cap_in, &in);
|
||||
|
||||
sp = new_CtrlStatus(cs);
|
||||
write_CtrlStatus(&cap_in, sp);
|
||||
capn_setp(capn_root(&c), 0, sp.p);
|
||||
|
||||
size_t sz = capn_size(&c);
|
||||
uint8_t* buf = (uint8_t*)malloc(sz);
|
||||
TEST_ASSERT_NOT_NULL(buf);
|
||||
TEST_ASSERT_EQUAL_size_t(sz, capn_write_mem(&c, buf, sz, 0));
|
||||
capn_free(&c);
|
||||
|
||||
struct capn dc;
|
||||
struct CtrlStatus cap_out;
|
||||
CtrlStatus_ptr dp;
|
||||
xce_ctrlstatus_t out;
|
||||
|
||||
capn_init_mem(&dc, buf, sz, 0);
|
||||
dp.p = capn_getp(capn_root(&dc), 0, 1);
|
||||
read_CtrlStatus(&cap_out, dp);
|
||||
memset(&out, 0, sizeof(out));
|
||||
decode_CtrlStatus(&out, &cap_out);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT64(UINT64_C(1781939830911), out.elected_at_ms);
|
||||
|
||||
free_CtrlStatus(&out);
|
||||
capn_free(&dc);
|
||||
free(buf);
|
||||
|
||||
printf("test_ctrlstatus_electedatms_roundtrip: PASS\n");
|
||||
}
|
||||
|
||||
/* ── (l) codec round-trip: HeartbeatMsg.electedAtMs @4 (task 7.1, Req 12.1) ──
|
||||
* Same design-delta schema addition on the heartbeat message the primary
|
||||
* already sends to standbys, next free ordinal after committedConfigTag @3.
|
||||
* Direct struct construction + encode/decode -- no heartbeat send/receive
|
||||
* logic is exercised (that is task 7.3, out of boundary for 7.1). Also proves
|
||||
* the pre-existing sibling fields are undisturbed by the new ordinal. */
|
||||
static void test_heartbeatmsg_electedatms_roundtrip(void)
|
||||
{
|
||||
heartbeat_msg_t in;
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.sender_priority = 1;
|
||||
in.term = 7;
|
||||
in.committed_lsn = 100;
|
||||
in.committed_config_tag = 200;
|
||||
in.elected_at_ms = UINT64_C(1781939830911); /* known non-zero value */
|
||||
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
struct HeartbeatMsg cap_in;
|
||||
HeartbeatMsg_ptr sp;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
|
||||
memset(&cap_in, 0, sizeof(cap_in));
|
||||
encode_HeartbeatMsg(cs, &cap_in, &in);
|
||||
|
||||
sp = new_HeartbeatMsg(cs);
|
||||
write_HeartbeatMsg(&cap_in, sp);
|
||||
capn_setp(capn_root(&c), 0, sp.p);
|
||||
|
||||
size_t sz = capn_size(&c);
|
||||
uint8_t* buf = (uint8_t*)malloc(sz);
|
||||
TEST_ASSERT_NOT_NULL(buf);
|
||||
TEST_ASSERT_EQUAL_size_t(sz, capn_write_mem(&c, buf, sz, 0));
|
||||
capn_free(&c);
|
||||
|
||||
struct capn dc;
|
||||
struct HeartbeatMsg cap_out;
|
||||
HeartbeatMsg_ptr dp;
|
||||
heartbeat_msg_t out;
|
||||
|
||||
capn_init_mem(&dc, buf, sz, 0);
|
||||
dp.p = capn_getp(capn_root(&dc), 0, 1);
|
||||
read_HeartbeatMsg(&cap_out, dp);
|
||||
memset(&out, 0, sizeof(out));
|
||||
decode_HeartbeatMsg(&out, &cap_out);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(1, out.sender_priority);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, out.term);
|
||||
TEST_ASSERT_EQUAL_UINT64(100, out.committed_lsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(200, out.committed_config_tag);
|
||||
TEST_ASSERT_EQUAL_UINT64(UINT64_C(1781939830911), out.elected_at_ms);
|
||||
|
||||
/* heartbeat_msg_t has no heap-owned fields (all scalars) -- no free_* call */
|
||||
capn_free(&dc);
|
||||
free(buf);
|
||||
|
||||
printf("test_heartbeatmsg_electedatms_roundtrip: PASS\n");
|
||||
}
|
||||
|
||||
/* ── suite entry point ─────────────────────────────────────── */
|
||||
|
||||
void test_ctrlstatus(void)
|
||||
|
|
@ -528,4 +650,6 @@ void test_ctrlstatus(void)
|
|||
RUN_TEST(test_ctrlstatus_standby_prelearn_committed_zero);
|
||||
RUN_TEST(test_ctrlstatus_primary_reports_own_committed);
|
||||
RUN_TEST(test_ctrlstatus_responder_time_ms);
|
||||
RUN_TEST(test_ctrlstatus_electedatms_roundtrip);
|
||||
RUN_TEST(test_heartbeatmsg_electedatms_roundtrip);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,6 +229,44 @@ static void test_ha_init_zeroes_standby_convergence_fields(void)
|
|||
printf("test_ha_init_zeroes_standby_convergence_fields: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Task 7.1 (controller-status delta, Req 12.1): ha_state_t.last_known_elected_at_ms
|
||||
* zero-initialised by ha_init ──────────────────────────────────────────────
|
||||
*
|
||||
* New HA state (design.md "Design Delta — Requirement 12", Locked Decision 2)
|
||||
* holding the currently-known primary's election time, declared alongside
|
||||
* last_known_committed_lsn/last_known_committed_config_tag. Stamped by
|
||||
* promotion (task 7.2, not yet wired) and learned via heartbeat (task 7.3,
|
||||
* not yet wired); this test only proves the field starts at the well-defined
|
||||
* zero/unknown value before any election, matching its siblings.
|
||||
*/
|
||||
static void test_ha_init_zeroes_last_known_elected_at_ms(void)
|
||||
{
|
||||
ha_state_t ha;
|
||||
ha_config_t cfg = {
|
||||
.heartbeat_interval_ms = 3000,
|
||||
.heartbeat_timeout_ms = 10000,
|
||||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
};
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 0));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT64(
|
||||
0, atomic_load_explicit(&ha.last_known_elected_at_ms,
|
||||
memory_order_acquire));
|
||||
|
||||
/* field must be writable (mirrors the sibling last_known_* fields) */
|
||||
atomic_store_explicit(&ha.last_known_elected_at_ms,
|
||||
UINT64_C(1781939830911), memory_order_release);
|
||||
TEST_ASSERT_EQUAL_UINT64(
|
||||
UINT64_C(1781939830911),
|
||||
atomic_load_explicit(&ha.last_known_elected_at_ms,
|
||||
memory_order_acquire));
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_ha_init_zeroes_last_known_elected_at_ms: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Task 3.1: ha_process_standby_state convergence tracking ─────────────── */
|
||||
|
||||
/*
|
||||
|
|
@ -991,6 +1029,7 @@ void test_ha_role(void)
|
|||
RUN_TEST(test_ha_init_zeroes_new_fields);
|
||||
/* Task 1.4: ha-repl-hardening per-standby convergence fields */
|
||||
RUN_TEST(test_ha_init_zeroes_standby_convergence_fields);
|
||||
RUN_TEST(test_ha_init_zeroes_last_known_elected_at_ms);
|
||||
/* Task 3.1: ha_process_standby_state convergence tracking */
|
||||
RUN_TEST(test_process_standby_state_converged);
|
||||
RUN_TEST(test_process_standby_state_lsn_behind);
|
||||
|
|
|
|||
|
|
@ -284,6 +284,8 @@ typedef struct {
|
|||
} peers;
|
||||
uint32_t peerCount;
|
||||
uint64_t responder_time_ms; /* @13: responder wall-clock at sample time (epoch ms) */
|
||||
uint64_t elected_at_ms; /* @14: current primary's election time (epoch ms),
|
||||
* learned off-primary via heartbeat; 0 = unknown */
|
||||
} xce_ctrlstatus_t;
|
||||
|
||||
/* ── Worker rejoin/resync job-state reconciliation (worker-rejoin-resync
|
||||
|
|
@ -613,6 +615,8 @@ struct heartbeat_msg_s {
|
|||
uint64_t term; /* sender's current term */
|
||||
uint64_t committed_lsn; /* Req 5: primary's committed WAL LSN */
|
||||
uint64_t committed_config_tag; /* Req 8: primary's committed config tag */
|
||||
uint64_t elected_at_ms; /* Req 12.2: primary's own last_known_elected_at_ms,
|
||||
* echoed on every heartbeat */
|
||||
};
|
||||
|
||||
/* CtrlHello state sync (Req 6.5, 6.6, 6.7) */
|
||||
|
|
|
|||
|
|
@ -551,6 +551,9 @@ struct CtrlStatus $C.mapname("xce_ctrlstatus_t") {
|
|||
peers @11: List(CtrlPeer) $C.mapname("peers.list") $C.maplistcount("peers.count");
|
||||
peerCount @12: UInt32;
|
||||
responderTimeMs @13: UInt64 $C.mapname("responder_time_ms"); # responder's wall-clock at sample time (epoch milliseconds)
|
||||
electedAtMs @14: UInt64 $C.mapname("elected_at_ms");
|
||||
# epoch millis of the current primary's election, learned off-primary via
|
||||
# heartbeat; 0/unset when no primary is known (gated together with primaryName)
|
||||
}
|
||||
|
||||
struct ClusterConf $C.mapname("xce_clusterconf_t") {
|
||||
|
|
@ -909,6 +912,8 @@ struct HeartbeatMsg $C.mapname("heartbeat_msg_t") {
|
|||
term @1: UInt64 $C.mapname("term");
|
||||
committedLsn @2: UInt64 $C.mapname("committed_lsn");
|
||||
committedConfigTag @3: UInt64 $C.mapname("committed_config_tag");
|
||||
electedAtMs @4: UInt64 $C.mapname("elected_at_ms");
|
||||
# primary's own last_known_elected_at_ms, echoed on every heartbeat (Req 12.2)
|
||||
}
|
||||
|
||||
# ── Config replication message structs ──────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue