feat(ha-heartbeat-liveness): role-based liveness reset in opcode dispatch (task 3.1)

Replace fd-based guard in OPCode_nop with role==CTRL_STANDBY check.
Add ha_on_heartbeat_received call to OPCode_hello ctrlhello path when standby.
Satisfies Req 1.3 (any message resets deadline) and Req 4.1 (ctrlhello counts).
3 new dispatch guard tests added to test_ha_liveness.c.
This commit is contained in:
Rongsong Shen 2026-06-01 15:31:15 +00:00
parent 622302a55c
commit d937c1c1bb
3 changed files with 104 additions and 3 deletions

View file

@ -196,6 +196,10 @@ static int myreq_handler(vtask_t* task)
if (request.body_which == Request_body_ctrlhellomsg) {
/* Extended OP_ctrlHello with CtrlHelloRequest body — state sync (Req 6.5–6.7) */
rc = handle_ctrlhello(c, ep, &request);
/* Req 4.1: ctrlhello counts as primary activity — reset liveness deadline. */
if ((ctrl_role_t)atomic_load_explicit(&c->ha.role, memory_order_acquire)
== CTRL_STANDBY)
ha_on_heartbeat_received(&c->ha);
} else if (request.body_which == Request_body_replymsg
&& request.body.replymsg
&& request.body.replymsg->data_which
@ -222,8 +226,10 @@ 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)
/* Req 1.3: any NOP from primary resets liveness deadline on standby.
* Role check is safe from any primary-connected ep — no fd comparison needed. */
if ((ctrl_role_t)atomic_load_explicit(&c->ha.role, memory_order_acquire)
== CTRL_STANDBY)
ha_on_heartbeat_received(&c->ha);
break;
case OPCode_join:

View file

@ -484,6 +484,98 @@ static void test_on_standby_connected_sets_last_push_time(void)
printf("test_on_standby_connected_sets_last_push_time: PASS\n");
}
/* ── Task 3.1: cluster.c dispatch guard tests ───────────────────────────── */
/*
* Simulates the cluster.c OPCode_nop dispatch guard:
* if role == CTRL_STANDBY → ha_on_heartbeat_received()
* Tests that the role-based guard (Req 1.3) correctly updates last_activity
* only when on a standby, and is a no-op when on a primary.
*/
/*
* Test 11: cluster.c nop dispatch guard — CTRL_STANDBY resets last_activity
*
* Req 1.3, 4.1: any primary message on a standby resets the liveness deadline.
* The cluster.c OPCode_nop handler must call ha_on_heartbeat_received when
* role == CTRL_STANDBY (not gated on ep->fd == primary_ctrl_fd).
*/
static void test_cluster_dispatch_nop_resets_activity_when_standby(void)
{
ha_state_t ha;
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_cfg, 1));
ha_do_demote(&ha); /* role = CTRL_STANDBY */
ha.last_activity = 0;
/* Simulate what cluster.c OPCode_nop handler now does:
* if (role == CTRL_STANDBY) ha_on_heartbeat_received(&ha); */
if ((ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire)
== CTRL_STANDBY)
ha_on_heartbeat_received(&ha);
TEST_ASSERT_NOT_EQUAL_MESSAGE(0, (int)ha.last_activity,
"Req 1.3: OPCode_nop on CTRL_STANDBY must reset last_activity");
ha_fini(&ha);
printf("test_cluster_dispatch_nop_resets_activity_when_standby: PASS\n");
}
/*
* Test 12: cluster.c nop dispatch guard — CTRL_PRIMARY does NOT reset last_activity
*
* Req 1.3: the guard must only call ha_on_heartbeat_received when CTRL_STANDBY.
* When role is CTRL_PRIMARY the call must be skipped so the primary does not
* accidentally update its own liveness clock via reflected nops.
*/
static void test_cluster_dispatch_nop_skips_when_primary(void)
{
ha_state_t ha;
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_cfg, 0));
/* Force role to CTRL_PRIMARY */
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY, memory_order_release);
ha.last_activity = 0;
/* Simulate cluster.c OPCode_nop guard: skip when not CTRL_STANDBY */
if ((ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire)
== CTRL_STANDBY)
ha_on_heartbeat_received(&ha);
TEST_ASSERT_EQUAL_INT_MESSAGE(0, (int)ha.last_activity,
"Req 1.3: OPCode_nop on CTRL_PRIMARY must NOT update last_activity");
ha_fini(&ha);
printf("test_cluster_dispatch_nop_skips_when_primary: PASS\n");
}
/*
* Test 13: cluster.c ctrlhello dispatch — CTRL_STANDBY resets last_activity (Req 4.1)
*
* Req 4.1: ctrlhello counts as primary activity — the OPCode_hello ctrlhello
* path must call ha_on_heartbeat_received when role == CTRL_STANDBY.
*/
static void test_cluster_dispatch_ctrlhello_resets_activity_when_standby(void)
{
ha_state_t ha;
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &g_cfg, 1));
ha_do_demote(&ha); /* role = CTRL_STANDBY */
ha.last_activity = 0;
/* Simulate cluster.c OPCode_hello ctrlhello path:
* rc = handle_ctrlhello(c, ep, &request);
* if (role == CTRL_STANDBY) ha_on_heartbeat_received(&ha); */
if ((ctrl_role_t)atomic_load_explicit(&ha.role, memory_order_acquire)
== CTRL_STANDBY)
ha_on_heartbeat_received(&ha);
TEST_ASSERT_NOT_EQUAL_MESSAGE(0, (int)ha.last_activity,
"Req 4.1: ctrlhello on CTRL_STANDBY must reset last_activity");
ha_fini(&ha);
printf("test_cluster_dispatch_ctrlhello_resets_activity_when_standby: PASS\n");
}
/* ── Suite entry point ──────────────────────────────────────────────────── */
void test_ha_liveness(void)
@ -499,5 +591,8 @@ void test_ha_liveness(void)
RUN_TEST(test_primary_hb_sends_nop_when_idle);
RUN_TEST(test_primary_hb_skips_nop_when_fresh);
RUN_TEST(test_on_standby_connected_sets_last_push_time);
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);
UNITY_END();
}