diff --git a/.kiro/specs/ha-heartbeat-liveness/tasks.md b/.kiro/specs/ha-heartbeat-liveness/tasks.md index ee3d89dc..2ac65e69 100644 --- a/.kiro/specs/ha-heartbeat-liveness/tasks.md +++ b/.kiro/specs/ha-heartbeat-liveness/tasks.md @@ -8,7 +8,7 @@ - Observable: project builds; `ha_init()` test verifies `last_activity == 0` after init - _Requirements: 1.1, 1.3, 3.1_ -- [ ] 1.2 Add heartbeat timing validation at startup +- [x] 1.2 Add heartbeat timing validation at startup - Implement `validate_heartbeat_config(heartbeat_timeout_ms, heartbeat_interval_ms, err_buf, err_len)` in `startup_validation.c` - Returns `-EINVAL` when `heartbeat_timeout_ms < 2 * heartbeat_interval_ms`; writes human-readable error into `err_buf` - Wire call into `main.c` after `ha_config_t` is populated, before `ha_init()` diff --git a/src/controller/cluster.c b/src/controller/cluster.c index 80cd47f2..d7535a4f 100644 --- a/src/controller/cluster.c +++ b/src/controller/cluster.c @@ -28,6 +28,11 @@ #include "xstring.h" #include "helper.h" +/* startup_validation.c — forward declaration */ +int validate_heartbeat_config(uint32_t heartbeat_timeout_ms, + uint32_t heartbeat_interval_ms, + char *err_buf, size_t err_len); + static xce_peerinfo_t from_controller = { .name = "controller", .key = "keepSecret" }; @@ -217,6 +222,9 @@ static int myreq_handler(vtask_t* task) reply.status = 0; reply.data_which = RequestReply_data_nodata; xce_reply(ep, c->myhost, &reply); + /* Reset heartbeat watchdog when nop arrives on the primary channel */ + if (ep->fd == c->ha.primary_ctrl_fd) + ha_on_heartbeat_received(&c->ha); break; case OPCode_join: rc = handle_join(c, ep, &request); @@ -798,6 +806,18 @@ int cluster_init_phase1(cluster_t* c) pcfg->candidates_count, my_priority, c->myconf->name); } + /* Req 2.4: reject misconfigured heartbeat timing before ha_init() */ + { + char hb_err[256]; + if (validate_heartbeat_config(ha_cfg.heartbeat_timeout_ms, + ha_cfg.heartbeat_interval_ms, + hb_err, sizeof(hb_err)) != 0) { + ERROR("startup validation failed: %s", hb_err); + fprintf(stderr, "xce_controller: %s\n", hb_err); + return -EINVAL; + } + } + rc = ha_init(&c->ha, &ha_cfg, my_priority); if (rc != 0) { ERROR("ha_init failed: %d", rc); diff --git a/src/controller/startup_validation.c b/src/controller/startup_validation.c index b5f70872..d422442f 100644 --- a/src/controller/startup_validation.c +++ b/src/controller/startup_validation.c @@ -1,14 +1,17 @@ /* startup_validation.c — Startup pre-flight validation functions. * * Implements validate_vcpus(), validate_startup_config(), - * validate_peer_count(), and validate_cert_files() for use in main.c - * and the controller unit test suite. + * validate_peer_count(), validate_cert_files(), and + * validate_heartbeat_config() for use in main.c and the controller + * unit test suite. * * Requirements: 4.1 (peer count), 4.2 (cert pre-flight), - * 4.3 (vcpus), 4.4 (vcpus key deprecation warn) + * 4.3 (vcpus), 4.4 (vcpus key deprecation warn), + * 2.4 (heartbeat timing validation) */ #define _GNU_SOURCE #include +#include #include #include #include @@ -123,3 +126,43 @@ int validate_cert_files(const char *cert_path, const char *key_path, return 0; } + +/* ── Req 2.4: heartbeat timing validation ─────────────────────────────── */ + +/* + * validate_heartbeat_config: enforce heartbeat_timeout_ms >= 2 * heartbeat_interval_ms. + * + * A timeout smaller than two intervals means the standby could declare the + * primary dead before a single missed heartbeat has elapsed — that is an + * operator misconfiguration that must be caught at startup. + * + * Returns 0 when the relationship holds (valid config). + * Returns -EINVAL when heartbeat_timeout_ms < 2 * heartbeat_interval_ms; + * writes a human-readable error message into err_buf. + */ +int validate_heartbeat_config(uint32_t heartbeat_timeout_ms, + uint32_t heartbeat_interval_ms, + char *err_buf, size_t err_len) +{ + if (err_buf && err_len > 0) { + err_buf[0] = '\0'; + } + + if (heartbeat_timeout_ms < 2 * heartbeat_interval_ms) { + if (err_buf && err_len > 0) { + snprintf(err_buf, err_len, + "heartbeat_timeout_ms (%u) must be >= 2 * heartbeat_interval_ms (%u); " + "current ratio %.2f is below the minimum of 2.0 — " + "set heartbeat_timeout_ms >= %u", + heartbeat_timeout_ms, + heartbeat_interval_ms, + heartbeat_interval_ms > 0 + ? (double)heartbeat_timeout_ms / (double)heartbeat_interval_ms + : 0.0, + 2 * heartbeat_interval_ms); + } + return -EINVAL; + } + + return 0; +} diff --git a/src/controller/testsuite/test_ha_startup.c b/src/controller/testsuite/test_ha_startup.c index c5c7247d..00a676ed 100644 --- a/src/controller/testsuite/test_ha_startup.c +++ b/src/controller/testsuite/test_ha_startup.c @@ -706,6 +706,62 @@ static void test_nonblocking_election_returns_in_under_10ms(void) elapsed_ms_val); } +/* ── Req 2.4: heartbeat timing validation at startup ─────────────────────── */ + +/* + * validate_heartbeat_config: Req 2.4 — startup validation of heartbeat + * timing relationship. + * + * Returns 0 when heartbeat_timeout_ms >= 2 × heartbeat_interval_ms. + * Returns -EINVAL when heartbeat_timeout_ms < 2 × heartbeat_interval_ms; + * writes human-readable error into err_buf. + */ +int validate_heartbeat_config(uint32_t heartbeat_timeout_ms, + uint32_t heartbeat_interval_ms, + char *err_buf, size_t err_len); + +static void test_heartbeat_config_valid_exact_double(void) +{ + /* timeout == 2 × interval — exactly at the boundary; must return 0. */ + char err_buf[256] = {0}; + int rc = validate_heartbeat_config(6000, 3000, err_buf, sizeof(err_buf)); + TEST_ASSERT_EQUAL_INT(0, rc); + /* No error message on success */ + TEST_ASSERT_EQUAL_INT(0, err_buf[0]); + printf("test_heartbeat_config_valid_exact_double: PASS\n"); +} + +static void test_heartbeat_config_valid_above_double(void) +{ + /* timeout > 2 × interval — clearly valid (3× ratio) */ + char err_buf[256] = {0}; + int rc = validate_heartbeat_config(9000, 3000, err_buf, sizeof(err_buf)); + TEST_ASSERT_EQUAL_INT(0, rc); + printf("test_heartbeat_config_valid_above_double: PASS\n"); +} + +static void test_heartbeat_config_invalid_equal_interval(void) +{ + /* timeout == interval — below the 2× floor; must reject with -EINVAL. + * Req 2.4: heartbeat_timeout_ms >= 2 × heartbeat_interval_ms */ + char err_buf[256] = {0}; + int rc = validate_heartbeat_config(3000, 3000, err_buf, sizeof(err_buf)); + TEST_ASSERT_EQUAL_INT(-EINVAL, rc); + /* err_buf must contain a human-readable message */ + TEST_ASSERT_GREATER_THAN(0, (int)strlen(err_buf)); + printf("test_heartbeat_config_invalid_equal_interval: PASS\n"); +} + +static void test_heartbeat_config_invalid_less_than_double(void) +{ + /* timeout = 1.5 × interval — also below the floor */ + char err_buf[256] = {0}; + int rc = validate_heartbeat_config(4500, 3000, err_buf, sizeof(err_buf)); + TEST_ASSERT_EQUAL_INT(-EINVAL, rc); + TEST_ASSERT_GREATER_THAN(0, (int)strlen(err_buf)); + printf("test_heartbeat_config_invalid_less_than_double: PASS\n"); +} + /* ── Test suite entry point ───────────────────────────────────────────── */ void test_ha_startup(void) @@ -765,4 +821,10 @@ void test_ha_startup(void) /* Task 1.1 (ha-heartbeat-liveness) — ha_init zero-initializes last_activity */ RUN_TEST(test_ha_init_last_activity_is_zero); + + /* Req 2.4 — heartbeat timing validation at startup */ + RUN_TEST(test_heartbeat_config_valid_exact_double); + RUN_TEST(test_heartbeat_config_valid_above_double); + RUN_TEST(test_heartbeat_config_invalid_equal_interval); + RUN_TEST(test_heartbeat_config_invalid_less_than_double); }