forked from lei.chen/myxce
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:
parent
f4327ce4f6
commit
282a82ff7e
4 changed files with 129 additions and 1 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ extern int xce_decode(io_packet_t*, xce_request_t*);
|
|||
extern int xce_reply(endpoint_t*, xce_peerinfo_t*, xce_reply_t*);
|
||||
extern int xce_hello_req(endpoint_t*, char*);
|
||||
extern int xce_nop_req(endpoint_t*);
|
||||
/* HA heartbeat: an OP_nop request carrying a HeartbeatMsg body with the
|
||||
* primary's committed position. Non-blocking, no reply awaited (the standby's
|
||||
* StateReport rides the normal nop reply). senderPriority is unused (0).
|
||||
* Generic xce_nop_req is left unchanged (body-less). */
|
||||
extern int xce_ha_heartbeat_req(endpoint_t* ep, uint64_t term,
|
||||
uint64_t committed_lsn,
|
||||
uint64_t committed_config_tag);
|
||||
extern int xce_shutdown_req(endpoint_t*);
|
||||
extern void init_reply_by_req(xce_reply_t*, xce_request_t*);
|
||||
extern int xce_join_req(endpoint_t*, int, hostattrs_t*);
|
||||
|
|
|
|||
|
|
@ -1138,6 +1138,37 @@ int xce_nop_req(endpoint_t* ep)
|
|||
return xce_request(ep, &request);
|
||||
}
|
||||
|
||||
/* xce_ha_heartbeat_req: send the HA heartbeat as an OP_nop request carrying a
|
||||
* HeartbeatMsg body with the primary's committed position. Clone of the nop
|
||||
* send path plus a heartbeatmsg body. Non-blocking, no reply awaited (the
|
||||
* standby's StateReport rides the normal nop reply). senderPriority is unused
|
||||
* (0). Generic xce_nop_req stays body-less. */
|
||||
int xce_ha_heartbeat_req(endpoint_t* ep, uint64_t term, uint64_t committed_lsn,
|
||||
uint64_t committed_config_tag)
|
||||
{
|
||||
heartbeat_msg_t hb = {
|
||||
.sender_priority = 0,
|
||||
.term = term,
|
||||
.committed_lsn = committed_lsn,
|
||||
.committed_config_tag = committed_config_tag,
|
||||
};
|
||||
/* clang-format off */
|
||||
xce_request_t request = {
|
||||
.opcode = OPCode_nop,
|
||||
.body_which = Request_body_heartbeatmsg,
|
||||
.body = {
|
||||
.heartbeatmsg = &hb
|
||||
}
|
||||
};
|
||||
/* clang-format on */
|
||||
|
||||
if (ep == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
return xce_request(ep, &request);
|
||||
}
|
||||
|
||||
int xce_hello_req(endpoint_t* ep, char* msg)
|
||||
{
|
||||
/* clang-format off */
|
||||
|
|
|
|||
|
|
@ -16,6 +16,89 @@ static void test_hostctrl_req_null_ep(void)
|
|||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
}
|
||||
|
||||
/* ── HA heartbeat sender (xce_ha_heartbeat_req) ───────────────────────────
|
||||
* The HA heartbeat is an OP_nop request carrying a HeartbeatMsg body. The
|
||||
* sender builds that request and hands it to xce_request(); since unit tests
|
||||
* have no live endpoint, these tests round-trip the same request shape the
|
||||
* sender builds through xce_encode()/xce_decode() and assert the three
|
||||
* committed-position fields survive the wire. A companion guard confirms the
|
||||
* generic xce_nop_req still produces a body-less nop. */
|
||||
|
||||
/* Build the heartbeat request exactly as xce_ha_heartbeat_req does and encode
|
||||
* it into a heap io_packet_t. xce_decode() takes ownership of the packet
|
||||
* (free_packet + FREEUP), so the caller must NOT free the returned pkt — it is
|
||||
* consumed by the subsequent xce_decode(). Mirrors the test_wal_wire pattern. */
|
||||
static io_packet_t *encode_heartbeat(uint64_t term, uint64_t committed_lsn,
|
||||
uint64_t committed_config_tag)
|
||||
{
|
||||
heartbeat_msg_t hb = {
|
||||
.sender_priority = 0,
|
||||
.term = term,
|
||||
.committed_lsn = committed_lsn,
|
||||
.committed_config_tag = committed_config_tag,
|
||||
};
|
||||
xce_request_t request = {
|
||||
.opcode = OPCode_nop,
|
||||
.body_which = Request_body_heartbeatmsg,
|
||||
.body = { .heartbeatmsg = &hb },
|
||||
};
|
||||
|
||||
io_packet_t *pkt = (io_packet_t *)calloc(1, sizeof(*pkt));
|
||||
TEST_ASSERT_NOT_NULL(pkt);
|
||||
/* xce_encode returns 1 on success (mirrors xce_request's encode path). */
|
||||
TEST_ASSERT_EQUAL_INT(1, xce_encode(&request, pkt));
|
||||
return pkt;
|
||||
}
|
||||
|
||||
static void test_ha_heartbeat_req_null_ep(void)
|
||||
{
|
||||
/* Mirror the nop sender's NULL-endpoint guard. */
|
||||
int rc = xce_ha_heartbeat_req(NULL, 1, 1, 1);
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
}
|
||||
|
||||
static void test_ha_heartbeat_req_roundtrip_fields(void)
|
||||
{
|
||||
/* Arrange/Act: encode a heartbeat carrying a committed position, decode. */
|
||||
io_packet_t *pkt = encode_heartbeat(7, 9, 4);
|
||||
|
||||
xce_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, &req)); /* consumes/frees pkt */
|
||||
|
||||
/* Assert: OP_nop + heartbeatmsg arm with the three fields intact. */
|
||||
TEST_ASSERT_EQUAL_INT(OPCode_nop, req.opcode);
|
||||
TEST_ASSERT_EQUAL_INT(Request_body_heartbeatmsg, req.body_which);
|
||||
TEST_ASSERT_NOT_NULL(req.body.heartbeatmsg);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, req.body.heartbeatmsg->term);
|
||||
TEST_ASSERT_EQUAL_UINT64(9, req.body.heartbeatmsg->committed_lsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, req.body.heartbeatmsg->committed_config_tag);
|
||||
|
||||
xce_free_request(&req);
|
||||
printf("test_ha_heartbeat_req_roundtrip_fields: PASS\n");
|
||||
}
|
||||
|
||||
/* Guard: the generic nop sender still produces a body-less nop request. */
|
||||
static void test_nop_req_remains_bodyless(void)
|
||||
{
|
||||
xce_request_t request
|
||||
= { .opcode = OPCode_nop, .body_which = Request_body_nomsg };
|
||||
|
||||
io_packet_t *pkt = (io_packet_t *)calloc(1, sizeof(*pkt));
|
||||
TEST_ASSERT_NOT_NULL(pkt);
|
||||
TEST_ASSERT_EQUAL_INT(1, xce_encode(&request, pkt));
|
||||
|
||||
xce_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, &req)); /* consumes/frees pkt */
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(OPCode_nop, req.opcode);
|
||||
TEST_ASSERT_EQUAL_INT(Request_body_nomsg, req.body_which);
|
||||
|
||||
xce_free_request(&req);
|
||||
printf("test_nop_req_remains_bodyless: PASS\n");
|
||||
}
|
||||
|
||||
static void test_hostctrl_req_null_ctrl(void)
|
||||
{
|
||||
/* non-NULL dummy ep is enough; the NULL ctrl check fires first */
|
||||
|
|
@ -105,4 +188,9 @@ void test_request(void)
|
|||
RUN_TEST(test_hostctrl_req_reject_untag);
|
||||
RUN_TEST(test_hostctrl_req_reject_negative_action);
|
||||
RUN_TEST(test_init_reply_by_req_zeroes_reply);
|
||||
|
||||
printf("\n>> HA heartbeat sender round-trip tests\n\n");
|
||||
RUN_TEST(test_ha_heartbeat_req_null_ep);
|
||||
RUN_TEST(test_ha_heartbeat_req_roundtrip_fields);
|
||||
RUN_TEST(test_nop_req_remains_bodyless);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue