diff --git a/.kiro/specs/controller-status/tasks.md b/.kiro/specs/controller-status/tasks.md index 1df0bc14..119e0cee 100644 --- a/.kiro/specs/controller-status/tasks.md +++ b/.kiro/specs/controller-status/tasks.md @@ -108,7 +108,7 @@ - _Boundary: promotion (ha_do_promote, raft_cb_become_leader)_ - _Depends: 7.1_ -- [ ] 7.3 Carry the primary's election time to standbys over the heartbeat +- [x] 7.3 Carry the primary's election time to standbys over the heartbeat - TDD (red first): a standby that processes a heartbeat carrying the primary's election time stores it as its own learned election-time and leaves every other learned/produced HA field (committed position, role, term, applied position) untouched (reconcile-free). - The primary includes its own election time in every heartbeat it sends; a standby that receives a heartbeat stores the carried election time as its learned value. - Observable: the reconcile-free test passes — only the learned election-time changes, nothing else moves; a live/loopback heartbeat round-trip shows the standby's learned election-time matching the primary's. diff --git a/src/controller/ha.c b/src/controller/ha.c index 62f94690..5a2aeb13 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -5901,12 +5901,14 @@ int (*ha_nop_req_hook)(endpoint_t *ep) = NULL; * ha_heartbeat_req_hook: test-only seam for the HA heartbeat send in * primary_heartbeat_fn. When non-NULL, primary_heartbeat_fn calls this INSTEAD * OF xce_ha_heartbeat_req with the SAME (term, committed_lsn, - * committed_config_tag) it stamps onto the wire, so tests can assert the - * heartbeat carries the primary's own current committed state. Req 1.1, 3.4. + * committed_config_tag, elected_at_ms) it stamps onto the wire, so tests can + * assert the heartbeat carries the primary's own current committed state and + * election time. Req 1.1, 3.4, 12.2. */ int (*ha_heartbeat_req_hook)(endpoint_t *ep, uint64_t term, uint64_t committed_lsn, - uint64_t committed_config_tag) = NULL; + uint64_t committed_config_tag, + uint64_t elected_at_ms) = NULL; /* * ha_reconnect_connect_hook: test-only seam for the BLOCKING half of the @@ -9257,6 +9259,19 @@ void ha_consume_heartbeat_committed(cluster_t *c, const heartbeat_msg_t *hb) ha_observe_heartbeat_committed(c, hb->committed_lsn, hb->committed_config_tag); + /* (Req 12.2) Learn the primary's election time. Unconditional overwrite — + * no monotonic/term compare needed, unlike the committed-position observe + * above: this call site already passed the non-primary gate + stale-term + * guard above, i.e. it is already gated on "this heartbeat is from my + * currently-recognized leader," the SAME precondition + * ha_observe_heartbeat_committed's own committed_lsn/committed_config_tag + * learn relies on. Deliberately NOT added inside + * ha_observe_heartbeat_committed itself (that function's contract/tests + * are unchanged) — a separate, unconditional store immediately alongside + * it, reconcile-free (touches nothing else). */ + atomic_store_explicit(&c->ha.last_known_elected_at_ms, hb->elected_at_ms, + memory_order_release); + /* 2026-07-10: drive this node's own committed_lsn + apply pump from the * position just learned (see ha_standby_advance_committed_and_apply) — * the commit-then-apply half of the standby ingest path. */ @@ -9874,6 +9889,14 @@ static int primary_heartbeat_fn(void *arg) uint64_t hb_committed_config_tag = atomic_load_explicit(&ha->committed_config_tag, memory_order_acquire); + /* Req 12.2: echo the primary's own currently-known election time + * on every heartbeat so standbys can learn it. Self-stamped by + * both promotion roads (task 7.2) at the instant this node + * committed to CTRL_PRIMARY; read unconditionally here, same + * shape as hb_term/hb_committed_lsn/hb_committed_config_tag. */ + uint64_t hb_elected_at_ms = + atomic_load_explicit(&ha->last_known_elected_at_ms, + memory_order_acquire); /* Endpoints evicted this tick; their repl deregistration runs AFTER * ha->lock is released (repl lock-ordering — mirrors the err handlers). */ @@ -9920,17 +9943,20 @@ static int primary_heartbeat_fn(void *arg) if (ha_heartbeat_req_hook) rc = ha_heartbeat_req_hook(ep, hb_term, hb_committed_lsn, - hb_committed_config_tag); + hb_committed_config_tag, + hb_elected_at_ms); else if (ha_nop_req_hook) rc = ha_nop_req_hook(ep); else rc = xce_ha_heartbeat_req(ep, hb_term, hb_committed_lsn, - hb_committed_config_tag); + hb_committed_config_tag, + hb_elected_at_ms); #else int rc = xce_ha_heartbeat_req(ep, hb_term, hb_committed_lsn, - hb_committed_config_tag); + hb_committed_config_tag, + hb_elected_at_ms); #endif if (rc < 0) WARN("HA: heartbeat nop to standby[%d] failed: %d", i, rc); diff --git a/src/controller/ha.h b/src/controller/ha.h index 1a997328..262a547e 100644 --- a/src/controller/ha.h +++ b/src/controller/ha.h @@ -2047,10 +2047,11 @@ extern int (*ha_push_config_hook)(void *c); /* * ha_heartbeat_req_hook: test-only seam for the per-standby HA heartbeat send * in primary_heartbeat_fn (Task 3.1). When non-NULL, primary_heartbeat_fn - * calls this INSTEAD OF xce_ha_heartbeat_req, passing the SAME three values it + * calls this INSTEAD OF xce_ha_heartbeat_req, passing the SAME four values it * stamps onto the wire — the primary's CURRENT own term / committed_lsn / - * committed_config_tag. A test installs it to capture those args and assert - * the heartbeat carries the primary's own committed state (Req 1.1, 3.4). + * committed_config_tag / elected_at_ms. A test installs it to capture those + * args and assert the heartbeat carries the primary's own committed state and + * election time (Req 1.1, 3.4, 12.2). * * Precedence (XCE_TESTING only): ha_heartbeat_req_hook (if set) takes priority, * else the legacy ha_nop_req_hook (if set), else the real xce_ha_heartbeat_req. @@ -2058,7 +2059,8 @@ extern int (*ha_push_config_hook)(void *c); */ extern int (*ha_heartbeat_req_hook)(endpoint_t *ep, uint64_t term, uint64_t committed_lsn, - uint64_t committed_config_tag); + uint64_t committed_config_tag, + uint64_t elected_at_ms); /* * ha_reconnect_connect_hook: test-only seam for the BLOCKING half of the diff --git a/src/controller/testsuite/test_ha_heartbeat_committed.c b/src/controller/testsuite/test_ha_heartbeat_committed.c index 64dc26a3..f2ff73e1 100644 --- a/src/controller/testsuite/test_ha_heartbeat_committed.c +++ b/src/controller/testsuite/test_ha_heartbeat_committed.c @@ -61,6 +61,12 @@ static uint64_t hb_tag(const cluster_t* c) memory_order_acquire); } +static uint64_t hb_elected(const cluster_t* c) +{ + return atomic_load_explicit(&c->ha.last_known_elected_at_ms, + memory_order_acquire); +} + /* ── tests ──────────────────────────────────────────────────── */ /* (a) Advance on a strictly higher carried LSN: both the LSN and the config tag @@ -332,6 +338,162 @@ static void test_consume_null_or_zero_noop(void) printf("test_consume_null_or_zero_noop: PASS\n"); } +/* ── controller-status delta (Req 12.2): learn the primary's election time + * off a received heartbeat ───────────────────────────────────────────────── + * + * ha_consume_heartbeat_committed's SAME call site that feeds + * ha_observe_heartbeat_committed(c, hb->committed_lsn, hb->committed_config_tag) + * also carries hb->elected_at_ms — a genuinely-RECEIVED heartbeat (the + * production caller is cluster.c's OP_nop dispatch) already gates on + * non-primary role + a non-stale term before either call, so the + * elected_at_ms store is a plain unconditional overwrite immediately + * alongside the existing committed-position learn — no separate monotonic/ + * term compare (design.md Locked Decision 2). ha_observe_heartbeat_committed + * itself is untouched; these tests exercise ONLY the new sibling store. + */ + +/* (i) A term-valid heartbeat stores the carried election time as the + * standby's own learned value (Req 12.2). */ +static void test_consume_stores_elected_at_ms(void) +{ + cluster_t c; + hb_setup_cluster(&c); + + atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY, + memory_order_release); + atomic_store_explicit(&c.ha.term, 5, memory_order_release); + atomic_store_explicit(&c.ha.last_known_elected_at_ms, 111, + memory_order_release); + + heartbeat_msg_t hb = { + .sender_priority = 0, + .term = 5, /* == standby term -> passes term-guard */ + .committed_lsn = 9, + .committed_config_tag = 0xAA, + .elected_at_ms = 222222, + }; + ha_consume_heartbeat_committed(&c, &hb); + + TEST_ASSERT_EQUAL_UINT64(222222, hb_elected(&c)); /* learned (12.2) */ + + hb_teardown_cluster(&c); + printf("test_consume_stores_elected_at_ms: PASS\n"); +} + +/* (j) Reconcile-free: a heartbeat carrying ONLY a new election time (its + * committed_lsn is 0, a liveness-only no-op for the committed observe) moves + * last_known_elected_at_ms and NOTHING else -- committed position, role, + * term, and applied position are all untouched. Mirrors + * test_observe_no_reconcile_no_other_mutation's shape for the new field + * (Req 12.2, 12.5). This is the RED test: it fails (elected_at_ms stays at + * its seeded value) before the store exists in ha_consume_heartbeat_committed. */ +static void test_consume_elected_at_ms_reconcile_free(void) +{ + cluster_t c; + hb_setup_cluster(&c); + + /* Seed every learned/produced HA field the store must NOT touch. */ + atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY, + memory_order_release); + atomic_store_explicit(&c.ha.term, 5, memory_order_release); + atomic_store_explicit(&c.ha.applied_lsn, 100, memory_order_release); + atomic_store_explicit(&c.persist.committed_lsn, 80, memory_order_release); + atomic_store_explicit(&c.persist.durable_lsn, 80, memory_order_release); + c.persist.current_lsn = 81; + atomic_store_explicit(&c.ha.committed_config_tag, 0xC0FFEE, + memory_order_release); + atomic_store_explicit(&c.ha.last_known_committed_lsn, 60, + memory_order_release); + atomic_store_explicit(&c.ha.last_known_committed_config_tag, 0x6060, + memory_order_release); + atomic_store_explicit(&c.ha.last_known_elected_at_ms, 1111, + memory_order_release); + + /* Snapshot everything that must remain byte-for-byte unchanged. */ + int32_t role0 = atomic_load_explicit(&c.ha.role, memory_order_acquire); + uint64_t term0 = atomic_load_explicit(&c.ha.term, memory_order_acquire); + uint64_t applied0 = atomic_load_explicit(&c.ha.applied_lsn, + memory_order_acquire); + uint64_t pcommit0 = atomic_load_explicit(&c.persist.committed_lsn, + memory_order_acquire); + uint64_t pdur0 = atomic_load_explicit(&c.persist.durable_lsn, + memory_order_acquire); + uint64_t ptip0 = c.persist.current_lsn; + uint64_t ctag0 = atomic_load_explicit(&c.ha.committed_config_tag, + memory_order_acquire); + uint64_t klsn0 = hb_lsn(&c); + uint64_t ktag0 = hb_tag(&c); + + heartbeat_msg_t hb = { + .sender_priority = 0, + .term = 5, /* == standby term -> passes term-guard */ + .committed_lsn = 0, /* liveness-only -> observe no-ops (4.2) */ + .committed_config_tag = 0xDEAD, + .elected_at_ms = 9999, + }; + ha_consume_heartbeat_committed(&c, &hb); + + /* ONLY last_known_elected_at_ms changed. */ + TEST_ASSERT_EQUAL_UINT64(9999, hb_elected(&c)); + + /* Committed position -- untouched (observe no-op on committed_lsn==0). */ + TEST_ASSERT_EQUAL_UINT64(klsn0, hb_lsn(&c)); + TEST_ASSERT_EQUAL_UINT64(ktag0, hb_tag(&c)); + + /* No truncation / no produced-state mutation. */ + TEST_ASSERT_EQUAL_UINT64(pcommit0, + atomic_load_explicit(&c.persist.committed_lsn, + memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(pdur0, + atomic_load_explicit(&c.persist.durable_lsn, + memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(ptip0, c.persist.current_lsn); + + /* No other HA state moved. */ + TEST_ASSERT_EQUAL_INT(role0, + atomic_load_explicit(&c.ha.role, memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(term0, + atomic_load_explicit(&c.ha.term, memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(applied0, + atomic_load_explicit(&c.ha.applied_lsn, + memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(ctag0, + atomic_load_explicit(&c.ha.committed_config_tag, + memory_order_acquire)); + + hb_teardown_cluster(&c); + printf("test_consume_elected_at_ms_reconcile_free: PASS\n"); +} + +/* (k) A stale-term heartbeat (hb->term < ha->term) must NOT store its carried + * election time either -- the elected_at_ms store sits behind the SAME + * term-guard early-return as the committed-position learn (3.2 shape). */ +static void test_consume_stale_term_does_not_store_elected_at_ms(void) +{ + cluster_t c; + hb_setup_cluster(&c); + + atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY, + memory_order_release); + atomic_store_explicit(&c.ha.term, 7, memory_order_release); + atomic_store_explicit(&c.ha.last_known_elected_at_ms, 4242, + memory_order_release); + + heartbeat_msg_t hb = { + .sender_priority = 0, + .term = 6, /* < standby term 7 -> fails term-guard */ + .committed_lsn = 99, + .committed_config_tag = 0xBB, + .elected_at_ms = 555555, + }; + ha_consume_heartbeat_committed(&c, &hb); + + TEST_ASSERT_EQUAL_UINT64(4242, hb_elected(&c)); /* unchanged (3.2 shape) */ + + hb_teardown_cluster(&c); + printf("test_consume_stale_term_does_not_store_elected_at_ms: PASS\n"); +} + /* ── suite entry point ─────────────────────────────────────── */ void test_ha_heartbeat_committed(void) @@ -346,4 +508,8 @@ void test_ha_heartbeat_committed(void) RUN_TEST(test_consume_stale_term_ignored); RUN_TEST(test_consume_primary_does_not_consume); RUN_TEST(test_consume_null_or_zero_noop); + /* controller-status delta (task 7.3): heartbeat-carried election time */ + RUN_TEST(test_consume_stores_elected_at_ms); + RUN_TEST(test_consume_elected_at_ms_reconcile_free); + RUN_TEST(test_consume_stale_term_does_not_store_elected_at_ms); } diff --git a/src/controller/testsuite/test_ha_liveness.c b/src/controller/testsuite/test_ha_liveness.c index c2447d58..de046424 100644 --- a/src/controller/testsuite/test_ha_liveness.c +++ b/src/controller/testsuite/test_ha_liveness.c @@ -319,13 +319,14 @@ extern int (*ha_nop_req_hook)(endpoint_t *ep); /* * ha_heartbeat_req_hook: XCE_TESTING hook (task 3.1). When non-NULL, * primary_heartbeat_fn calls this with the (term, committed_lsn, - * committed_config_tag) it stamps onto the heartbeat, so a test can assert the - * heartbeat carries the primary's OWN current committed state. Declared in - * ha.h. + * committed_config_tag, elected_at_ms) it stamps onto the heartbeat, so a + * test can assert the heartbeat carries the primary's OWN current committed + * state and election time (Req 12.2). Declared in ha.h. */ extern int (*ha_heartbeat_req_hook)(endpoint_t *ep, uint64_t term, uint64_t committed_lsn, - uint64_t committed_config_tag); + uint64_t committed_config_tag, + uint64_t elected_at_ms); /* * primary_heartbeat_fn_for_test: test entry-point that exercises the primary @@ -359,16 +360,19 @@ static int g_hb_calls = 0; static uint64_t g_hb_term = 0; static uint64_t g_hb_committed_lsn = 0; static uint64_t g_hb_committed_config_tag = 0; +static uint64_t g_hb_elected_at_ms = 0; static int stub_heartbeat_req(endpoint_t *ep, uint64_t term, uint64_t committed_lsn, - uint64_t committed_config_tag) + uint64_t committed_config_tag, + uint64_t elected_at_ms) { (void)ep; g_hb_calls++; g_hb_term = term; g_hb_committed_lsn = committed_lsn; g_hb_committed_config_tag = committed_config_tag; + g_hb_elected_at_ms = elected_at_ms; return 0; } @@ -599,11 +603,16 @@ static void test_primary_hb_stamps_own_committed_and_term(void) const uint64_t PRIMARY_TERM = 7; const uint64_t PRIMARY_COMMITTED_LSN = 42; const uint64_t PRIMARY_COMMITTED_TAG = 13; + const uint64_t PRIMARY_ELECTED_AT_MS = 1700000000123ULL; atomic_store_explicit(&c.ha.term, PRIMARY_TERM, memory_order_release); atomic_store_explicit(&c.persist.committed_lsn, PRIMARY_COMMITTED_LSN, memory_order_release); atomic_store_explicit(&c.ha.committed_config_tag, PRIMARY_COMMITTED_TAG, memory_order_release); + /* Req 12.2: the primary's own currently-known election time (self-stamped + * by promotion, task 7.2) must be echoed onto the outgoing heartbeat. */ + atomic_store_explicit(&c.ha.last_known_elected_at_ms, PRIMARY_ELECTED_AT_MS, + memory_order_release); /* Poison the standby-side last_known_* fields with DIFFERENT values: a * heartbeat that mistakenly read these (the 3.4 bug) would carry them. */ @@ -622,6 +631,7 @@ static void test_primary_hb_stamps_own_committed_and_term(void) g_hb_calls = 0; g_hb_term = g_hb_committed_lsn = g_hb_committed_config_tag = 0; + g_hb_elected_at_ms = 0; ha_heartbeat_req_hook = stub_heartbeat_req; primary_heartbeat_fn_for_test(&c); @@ -636,6 +646,8 @@ static void test_primary_hb_stamps_own_committed_and_term(void) "Req 1.1/3.4: heartbeat must carry the primary's OWN persist.committed_lsn"); TEST_ASSERT_EQUAL_UINT64_MESSAGE(PRIMARY_COMMITTED_TAG, g_hb_committed_config_tag, "Req 1.1/3.4: heartbeat must carry the primary's OWN committed_config_tag"); + TEST_ASSERT_EQUAL_UINT64_MESSAGE(PRIMARY_ELECTED_AT_MS, g_hb_elected_at_ms, + "Req 12.2: heartbeat must carry the primary's OWN last_known_elected_at_ms"); /* last_push_time still updated on success — bookkeeping unchanged. */ TEST_ASSERT_NOT_EQUAL_MESSAGE(0, (int)sctx->last_push_time, diff --git a/src/controller/testsuite/test_ha_partition.c b/src/controller/testsuite/test_ha_partition.c index ee6ef712..df9ad312 100644 --- a/src/controller/testsuite/test_ha_partition.c +++ b/src/controller/testsuite/test_ha_partition.c @@ -44,10 +44,12 @@ extern int (*ha_reconnect_connect_hook)(cluster_t *c, int peer_idx); extern int primary_heartbeat_fn_for_test(cluster_t *c); extern int standby_ep_ctx_build_for_test(endpoint_t *ep, ha_state_t *ha, cluster_t *c, int peer_idx); -/* ha_heartbeat_req_hook: per-standby stamp hook (endpoint, term, committed_lsn, tag). */ +/* ha_heartbeat_req_hook: per-standby stamp hook (endpoint, term, committed_lsn, + * tag, elected_at_ms). */ extern int (*ha_heartbeat_req_hook)(endpoint_t *ep, uint64_t term, uint64_t committed_lsn, - uint64_t committed_config_tag); + uint64_t committed_config_tag, + uint64_t elected_at_ms); extern int (*ha_nop_req_hook)(endpoint_t *ep); @@ -195,9 +197,11 @@ static int ps_heal_nop_hook(endpoint_t *ep) * standby_endpoints so it's never reached. Only installed during partition; * it must never block the tick. */ static int partition_hb_hook(endpoint_t *ep, uint64_t term, - uint64_t committed_lsn, uint64_t committed_config_tag) + uint64_t committed_lsn, uint64_t committed_config_tag, + uint64_t elected_at_ms) { (void)ep; (void)term; (void)committed_lsn; (void)committed_config_tag; + (void)elected_at_ms; atomic_fetch_add(&g_survivor_nops, 1); return 0; } diff --git a/src/include/request.h b/src/include/request.h index a39ebefd..01aacb52 100644 --- a/src/include/request.h +++ b/src/include/request.h @@ -27,12 +27,15 @@ extern int xce_reply(endpoint_t*, xce_peerinfo_t*, xce_reply_t*); extern int xce_hello_req(endpoint_t*, char*); extern int xce_nop_req(endpoint_t*); /* HA heartbeat: an OP_nop request carrying a HeartbeatMsg body with the - * primary's committed position. Non-blocking, no reply awaited (the standby's - * StateReport rides the normal nop reply). senderPriority is unused (0). - * Generic xce_nop_req is left unchanged (body-less). */ + * primary's committed position and its own election time (Req 12.2 — echoed + * to standbys so they can learn the currently-known primary's election + * time). Non-blocking, no reply awaited (the standby's StateReport rides the + * normal nop reply). senderPriority is unused (0). Generic xce_nop_req is + * left unchanged (body-less). */ extern int xce_ha_heartbeat_req(endpoint_t* ep, uint64_t term, uint64_t committed_lsn, - uint64_t committed_config_tag); + uint64_t committed_config_tag, + uint64_t elected_at_ms); extern int xce_shutdown_req(endpoint_t*); extern void init_reply_by_req(xce_reply_t*, xce_request_t*); extern int xce_join_req(endpoint_t*, int, hostattrs_t*); diff --git a/src/lib/request.c b/src/lib/request.c index d2063365..27a252a6 100644 --- a/src/lib/request.c +++ b/src/lib/request.c @@ -1347,18 +1347,20 @@ int xce_nop_req(endpoint_t* ep) } /* xce_ha_heartbeat_req: send the HA heartbeat as an OP_nop request carrying a - * HeartbeatMsg body with the primary's committed position. Clone of the nop - * send path plus a heartbeatmsg body. Non-blocking, no reply awaited (the - * standby's StateReport rides the normal nop reply). senderPriority is unused - * (0). Generic xce_nop_req stays body-less. */ + * HeartbeatMsg body with the primary's committed position and its own + * election time (Req 12.2). Clone of the nop send path plus a heartbeatmsg + * body. Non-blocking, no reply awaited (the standby's StateReport rides the + * normal nop reply). senderPriority is unused (0). Generic xce_nop_req stays + * body-less. */ int xce_ha_heartbeat_req(endpoint_t* ep, uint64_t term, uint64_t committed_lsn, - uint64_t committed_config_tag) + uint64_t committed_config_tag, uint64_t elected_at_ms) { heartbeat_msg_t hb = { .sender_priority = 0, .term = term, .committed_lsn = committed_lsn, .committed_config_tag = committed_config_tag, + .elected_at_ms = elected_at_ms, }; /* clang-format off */ xce_request_t request = { diff --git a/src/lib/testsuite/test_request.c b/src/lib/testsuite/test_request.c index c44d6f77..a8c8aef0 100644 --- a/src/lib/testsuite/test_request.c +++ b/src/lib/testsuite/test_request.c @@ -29,13 +29,15 @@ static void test_hostctrl_req_null_ep(void) * (free_packet + FREEUP), so the caller must NOT free the returned pkt — it is * consumed by the subsequent xce_decode(). Mirrors the test_wal_wire pattern. */ static io_packet_t *encode_heartbeat(uint64_t term, uint64_t committed_lsn, - uint64_t committed_config_tag) + uint64_t committed_config_tag, + uint64_t elected_at_ms) { heartbeat_msg_t hb = { .sender_priority = 0, .term = term, .committed_lsn = committed_lsn, .committed_config_tag = committed_config_tag, + .elected_at_ms = elected_at_ms, }; xce_request_t request = { .opcode = OPCode_nop, @@ -53,26 +55,28 @@ static io_packet_t *encode_heartbeat(uint64_t term, uint64_t committed_lsn, static void test_ha_heartbeat_req_null_ep(void) { /* Mirror the nop sender's NULL-endpoint guard. */ - int rc = xce_ha_heartbeat_req(NULL, 1, 1, 1); + int rc = xce_ha_heartbeat_req(NULL, 1, 1, 1, 1); TEST_ASSERT_EQUAL_INT(-EINVAL, rc); } static void test_ha_heartbeat_req_roundtrip_fields(void) { - /* Arrange/Act: encode a heartbeat carrying a committed position, decode. */ - io_packet_t *pkt = encode_heartbeat(7, 9, 4); + /* Arrange/Act: encode a heartbeat carrying a committed position and an + * election time (Req 12.2), decode. */ + io_packet_t *pkt = encode_heartbeat(7, 9, 4, 12345); xce_request_t req; memset(&req, 0, sizeof(req)); TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, &req)); /* consumes/frees pkt */ - /* Assert: OP_nop + heartbeatmsg arm with the three fields intact. */ + /* Assert: OP_nop + heartbeatmsg arm with all four fields intact. */ TEST_ASSERT_EQUAL_INT(OPCode_nop, req.opcode); TEST_ASSERT_EQUAL_INT(Request_body_heartbeatmsg, req.body_which); TEST_ASSERT_NOT_NULL(req.body.heartbeatmsg); TEST_ASSERT_EQUAL_UINT64(7, req.body.heartbeatmsg->term); TEST_ASSERT_EQUAL_UINT64(9, req.body.heartbeatmsg->committed_lsn); TEST_ASSERT_EQUAL_UINT64(4, req.body.heartbeatmsg->committed_config_tag); + TEST_ASSERT_EQUAL_UINT64(12345, req.body.heartbeatmsg->elected_at_ms); xce_free_request(&req); printf("test_ha_heartbeat_req_roundtrip_fields: PASS\n");