From e3ca775f0d4618be868b11c63af5112816d1a107 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 4 Jun 2026 14:14:23 +0000 Subject: [PATCH] feat(ha-repl-hardening): add per-standby convergence tracking arrays (task 1.4) Add standby_reported_tag/lsn/converged arrays to ha_state_t, sized MAX_HA_CANDIDATES. Zero-init via existing memset in ha_init(). Unit test verifies zero-init and writability. --- .kiro/specs/ha-repl-hardening/tasks.md | 2 +- src/controller/ha.c | 5 +++ src/controller/ha.h | 4 +++ src/controller/testsuite/test_ha_role.c | 45 +++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.kiro/specs/ha-repl-hardening/tasks.md b/.kiro/specs/ha-repl-hardening/tasks.md index 103ed30e..2f830507 100644 --- a/.kiro/specs/ha-repl-hardening/tasks.md +++ b/.kiro/specs/ha-repl-hardening/tasks.md @@ -32,7 +32,7 @@ - _Requirements: 4.1, 4.2_ - _Boundary: Wire Protocol (controller.capnp)_ -- [ ] 1.4 Add per-standby convergence tracking fields to `ha_state_t` +- [x] 1.4 Add per-standby convergence tracking fields to `ha_state_t` - Add `standby_reported_tag[MAX_HA_CANDIDATES]`, `standby_reported_lsn[MAX_HA_CANDIDATES]`, `standby_converged[MAX_HA_CANDIDATES]` arrays - Zero-initialize in `ha_init()`; these fields are read by the convergence evaluator in task 3.1 - Unit test: verify fields are zero after `ha_init()` and writable from test context diff --git a/src/controller/ha.c b/src/controller/ha.c index 3ca06d27..ec4ca2b8 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -689,6 +689,11 @@ int ha_init(ha_state_t *ha, const ha_config_t *cfg, int my_priority) * Written by ha_on_heartbeat_received (task 2.1); read by liveness_poll_fn. */ ha->last_activity = 0; + /* ha-repl-hardening Task 1.4: per-standby convergence tracking arrays. + * memset(ha, 0, sizeof(*ha)) above already zeroes these arrays; the explicit + * note documents the invariant: on entry to CTRL_PRIMARY_COMMITTING every + * standby starts as not-converged (tag=0, lsn=0, converged=false). */ + /* heartbeat_seen starts at 0: no heartbeat seen yet. * ha_wait_for_heartbeat_or_timeout() calls sem_timedwait on this. */ sem_init(&ha->heartbeat_seen, 0, 0); diff --git a/src/controller/ha.h b/src/controller/ha.h index 9512ed27..e8492696 100644 --- a/src/controller/ha.h +++ b/src/controller/ha.h @@ -177,6 +177,10 @@ typedef struct { void *committing_timeout_ctx; /* committing_timeout_ctx_t* for cancel on step-down */ /* ── New fields added by ha-heartbeat-liveness spec (Task 1.1) ── */ time_t last_activity; /* CLOCK_MONOTONIC seconds of last primary message; 0 = none yet */ + /* ── New fields added by ha-repl-hardening spec (Task 1.4) ──────── */ + uint64_t standby_reported_tag[MAX_HA_CANDIDATES]; /* Last config_tag reported by each standby */ + uint64_t standby_reported_lsn[MAX_HA_CANDIDATES]; /* Last applied_lsn reported by each standby */ + bool standby_converged[MAX_HA_CANDIDATES]; /* Whether standby[i] has converged with primary */ } ha_state_t; /* ── Public API ────────────────────────────────────────────── */ diff --git a/src/controller/testsuite/test_ha_role.c b/src/controller/testsuite/test_ha_role.c index c22c690b..52f9fd3c 100644 --- a/src/controller/testsuite/test_ha_role.c +++ b/src/controller/testsuite/test_ha_role.c @@ -194,6 +194,49 @@ static void test_ha_init_zeroes_new_fields(void) printf("test_ha_init_zeroes_new_fields: PASS\n"); } +/* + * Task 1.4: per-standby convergence tracking fields zero-initialised by ha_init. + * + * Verifies: + * - standby_reported_tag[i] == 0 for all i after ha_init() + * - standby_reported_lsn[i] == 0 for all i after ha_init() + * - standby_converged[i] == false for all i after ha_init() + * - fields are writable from test context + * + * Requirements: 2.1 (ha-repl-hardening spec) + */ +static void test_ha_init_zeroes_standby_convergence_fields(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, + .committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS, + }; + TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 0)); + + /* All per-standby tracking arrays must be zero/false after ha_init */ + for (int i = 0; i < MAX_HA_CANDIDATES; i++) { + TEST_ASSERT_EQUAL_UINT64(0, ha.standby_reported_tag[i]); + TEST_ASSERT_EQUAL_UINT64(0, ha.standby_reported_lsn[i]); + TEST_ASSERT_FALSE(ha.standby_converged[i]); + } + + /* Fields must be writable */ + ha.standby_reported_tag[0] = 42ULL; + ha.standby_reported_lsn[0] = 100ULL; + ha.standby_converged[0] = true; + TEST_ASSERT_EQUAL_UINT64(42ULL, ha.standby_reported_tag[0]); + TEST_ASSERT_EQUAL_UINT64(100ULL, ha.standby_reported_lsn[0]); + TEST_ASSERT_TRUE(ha.standby_converged[0]); + + ha_fini(&ha); + printf("test_ha_init_zeroes_standby_convergence_fields: PASS\n"); +} + void test_ha_role(void) { RUN_TEST(test_ha_init_default_role_is_standby); @@ -207,6 +250,8 @@ void test_ha_role(void) /* Watchdog / SIGUSR1 tests deleted */ /* Task 2.1: election-iomgr-refactor new fields */ 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); } /* test_am_i_primary suite deleted — am_i_primary() removed (election-iomgr-refactor) */