diff --git a/.kiro/specs/ha-heartbeat-liveness/tasks.md b/.kiro/specs/ha-heartbeat-liveness/tasks.md index 56d64097..0b847e5d 100644 --- a/.kiro/specs/ha-heartbeat-liveness/tasks.md +++ b/.kiro/specs/ha-heartbeat-liveness/tasks.md @@ -48,7 +48,7 @@ - _Depends: 2.1_ - _Boundary: cluster.c (opcode dispatch)_ -- [ ] 3.2 Verify config_repl.c ctrlhello integration and EPOLLRDHUP cancellation path +- [x] 3.2 Verify config_repl.c ctrlhello integration and EPOLLRDHUP cancellation path - Confirm `ha_arm_heartbeat_timeout_vtask` call in `handle_ctrlhello` (config_repl.c:1380) still correctly arms the delaymgr poll (not a thread) - Confirm `ha_on_heartbeat_received` call at config_repl.c:1379 writes `last_activity` (new impl) - Confirm `ha_on_primary_ctrl_fd_rdhup` now calls `ha_disarm_heartbeat_timeout_vtask` as its first step (Req 5.4) diff --git a/src/controller/testsuite/test_ha_liveness.c b/src/controller/testsuite/test_ha_liveness.c index de97ace4..ff77a434 100644 --- a/src/controller/testsuite/test_ha_liveness.c +++ b/src/controller/testsuite/test_ha_liveness.c @@ -576,6 +576,64 @@ static void test_cluster_dispatch_ctrlhello_resets_activity_when_standby(void) printf("test_cluster_dispatch_ctrlhello_resets_activity_when_standby: PASS\n"); } +/* ── Test 14: ha_arm_heartbeat_timeout_vtask does not increase thread count ── */ + +/* + * Task 3.2, Req 1.6: ha_arm_heartbeat_timeout_vtask must NOT spawn a pthread. + * + * We read the process thread count from /proc/self/status before and after + * calling the arm function and assert the count is unchanged. This is a + * stronger check than test_ha_arm_no_crash_no_thread (Test 6), which only + * verifies no crash and last_activity written. + * + * Note: with c==NULL the delaymgr is not running; delay_task_ms will fail + * silently (no actual delaymgr task is queued), but the arm path executes and + * must not create a thread as a side-effect. + */ +static int read_proc_thread_count(void) +{ + FILE *f = fopen("/proc/self/status", "r"); + if (!f) return -1; + char line[128]; + int count = -1; + while (fgets(line, sizeof(line), f)) { + if (strncmp(line, "Threads:", 8) == 0) { + if (sscanf(line + 8, "%d", &count) != 1) + count = -1; + break; + } + } + fclose(f); + return count; +} + +static void test_ha_arm_does_not_increase_thread_count(void) +{ + ha_state_t ha; + TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_cfg, 1)); + ha_do_demote(&ha); + + int threads_before = read_proc_thread_count(); + TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, threads_before, + "could not read /proc/self/status Threads"); + + /* Call arm — must not spawn a pthread (Req 1.6) */ + ha_arm_heartbeat_timeout_vtask(&ha, NULL); + + int threads_after = read_proc_thread_count(); + TEST_ASSERT_NOT_EQUAL_MESSAGE(-1, threads_after, + "could not read /proc/self/status Threads after arm"); + + TEST_ASSERT_EQUAL_INT_MESSAGE(threads_before, threads_after, + "Req 1.6: ha_arm_heartbeat_timeout_vtask must not spawn a pthread"); + + /* Disarm to release the ctx allocation */ + ha_disarm_heartbeat_timeout_vtask(&ha); + + ha_fini(&ha); + printf("test_ha_arm_does_not_increase_thread_count: PASS\n"); +} + /* ── Suite entry point ──────────────────────────────────────────────────── */ void test_ha_liveness(void) @@ -594,5 +652,6 @@ void test_ha_liveness(void) RUN_TEST(test_cluster_dispatch_nop_resets_activity_when_standby); RUN_TEST(test_cluster_dispatch_nop_skips_when_primary); RUN_TEST(test_cluster_dispatch_ctrlhello_resets_activity_when_standby); + RUN_TEST(test_ha_arm_does_not_increase_thread_count); UNITY_END(); }