feat(ha-heartbeat-committed-propagation): add xce_ha_heartbeat_req sender (task 1.2)

Send the HA heartbeat as OP_nop + the existing HeartbeatMsg body carrying
{term, committed_lsn, committed_config_tag} (senderPriority unused). Generic
xce_nop_req untouched; C-side body support pre-existed (no typemap edit). Req 1.1
producer half. Round-trip test proves the 3 fields survive the wire; lib suite
0 failures under ASAN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-06-20 11:40:31 +00:00
parent f4327ce4f6
commit 282a82ff7e
4 changed files with 129 additions and 1 deletions

View file

@ -9,7 +9,7 @@
- Observable: the controller suite builds clean and passes unchanged; every read/write of the field uses atomic accessors (no plain access remains).
- _Requirements: 2.3_
- _Boundary: ha.h + last_known_committed_config_tag accessors_
- [ ] 1.2 Add the HA heartbeat sender `xce_ha_heartbeat_req`
- [x] 1.2 Add the HA heartbeat sender `xce_ha_heartbeat_req`
- Add `xce_ha_heartbeat_req(endpoint_t* ep, uint64_t term, uint64_t committed_lsn, uint64_t committed_config_tag)` (`src/include/request.h` + `src/lib/request.c`) that sends an `OPCode_nop` request carrying a `heartbeatmsg` body (`HeartbeatMsg{term, committedLsn, committedConfigTag}`; `senderPriority` unused). Leave the generic `xce_nop_req` untouched.
- TDD: a round-trip test encodes via the new sender and decodes a request with `body_which == Request_body_heartbeatmsg` carrying the three field values; a guard assertion confirms `xce_nop_req` still sends a bodyless nop.
- Observable: the round-trip test decodes the three fields from the new sender, and the bodyless-`xce_nop_req` guard still holds.
@ -57,4 +57,6 @@
- The HA heartbeat is `OPCode_nop` with a `heartbeatmsg` body — opcode and body are independent in this schema (mirrors `OP_hello` carrying `ctrlhellomsg` vs `replymsg`). Dispatch routes by `OPCode_nop` (unchanged); the consumer branches on `body_which`.
- Do NOT reuse `cluster_apply_ctrlhello_reply` for the heartbeat path — it truncates (`ha_rejoin_reconcile`). The heartbeat updater is reconcile-free (Req 3.1).
- Do NOT touch the client keepalive `OPCode_heartbeat` / `xce_heartbeat` (a different message) or the generic `xce_nop_req` (client nop).
- 1.1 LEARNING: `ha.h` is `src/controller/ha.h` (NOT `src/controller/include/ha.h`). `last_known_committed_config_tag` is now `_Atomic uint64_t` (`ha.h:172`), sibling of `last_known_committed_lsn` (`ha.h:170`); read via `atomic_load_explicit(acquire)`, write via `atomic_store_explicit(release)`.
- 1.2 LEARNING: the C-side `heartbeatmsg` body support PRE-EXISTED (no typemap edit needed) — `heartbeat_msg_t` typedef `typemap.h:17`, `xce_request_t.heartbeatmsg` member `typemap.h:287`, `struct heartbeat_msg_s{sender_priority,term,committed_lsn,committed_config_tag}` `typemap.h:445` (order matches schema `@0..@3`); enum `Request_body_heartbeatmsg == 22` (rides `OPCode_nop`). The codec (`encode/decode/free_HeartbeatMsg` + the request arm) was generated; only producer/consumer were missing. Test round-trip pattern: heap-`calloc` packet consumed+freed by `xce_decode` (caller does NOT free pkt), decoded body freed by `xce_free_request` (see `test_wal_wire.c`). Sender uses `xce_request(ep, &req)` (non-blocking).
- The vote-floor formula is NOT edited — task 3.2 is a verification test only. Safety rests on the term-guard (task 2.2) + monotonicity (task 2.1) + cluster-monotonic committed; see design.md Security Considerations.