feat: P2-4 wire repl_broadcast into persist_wal_async/sync_append; last_replicated_crc

- Move repl.h from controller/ to include/ (shared header) to break
  persist.h ↔ repl.h circular include
- Extract wal_entry_hdr_t + wal_entry_type into include/wal_types.h so
  repl.h can include only what it needs without pulling in all of persist.h
- Add replicator_t repl field to persist_t (embedded by value, P2-4)
- Wire repl_init/repl_fini into persist_init/persist_fini lifecycle
- Wire repl_broadcast into persist_wal_async after each WAL write;
  last_replicated_crc updated here (not in persist_write_checkpoint_marker)
- Wire repl_wait_ack into persist_wal_sync_append; returns 0 on local
  WAL success; sync_rc_out carries -ENODEV/-ETIMEDOUT for degraded mode
- Add 4 TDD tests in test_persist_repl.c: structural repl field check,
  async no-standbys succeeds, sync degraded mode rc, checkpoint CRC invariant
This commit is contained in:
Rongsong Shen 2026-05-07 17:54:48 +00:00
parent d326ee1b2e
commit 3563d7387b
7 changed files with 228 additions and 41 deletions

View file

@ -168,6 +168,7 @@ int persist_init(persist_t* p, persist_config_t* cfg, cluster_t* c)
pthread_mutex_init(&p->lock, NULL);
pthread_mutex_init(&p->checkpoint_mutex, NULL);
pthread_cond_init(&p->checkpoint_done, NULL);
repl_init(&p->repl); /* P2-4: fan-out replicator lifecycle */
fprintf(stderr,
"INFO: WAL/snapshot stored unencrypted in %s — "
@ -214,6 +215,7 @@ void persist_stop(persist_t* p)
void persist_fini(persist_t* p)
{
repl_fini(&p->repl); /* P2-4: fan-out replicator lifecycle */
pthread_mutex_destroy(&p->lock);
pthread_mutex_destroy(&p->checkpoint_mutex);
pthread_cond_destroy(&p->checkpoint_done);
@ -274,8 +276,15 @@ int persist_wal_async(persist_t* p, uint32_t type,
p->seg_offset += entry_size;
p->total_wal_bytes += entry_size;
p->last_crc = hdr.crc;
/* For single-controller P0 there is no replication; track same value */
p->last_replicated_crc = hdr.crc;
/* Replicate to standbys — never replicate WAL_CHECKPOINT entries.
* last_replicated_crc is distinct from last_crc:
* last_crc includes checkpoint markers; last_replicated_crc skips them.
* This maintains a continuous CRC chain for standbys (design doc Part 2.3).
* Note: WAL_CHECKPOINT is blocked by the assert at function entry, so
* every entry that reaches here is a replicable entry. */
p->last_replicated_crc = hdr.crc;
repl_broadcast(&p->repl, &hdr, payload, len);
/* Rotate segment when full */
if (p->seg_offset >= WAL_SEGMENT_SIZE_BYTES) {
@ -845,9 +854,26 @@ int persist_wal_sync_append(persist_t* p, uint32_t type,
int* sync_rc_out)
{
int rc = persist_wal_async(p, type, payload, len);
if (rc != 0) {
if (sync_rc_out)
*sync_rc_out = rc;
return rc;
}
/* For sync replication (WAL_JOB_SUBMIT): wait for at least one standby ACK.
* We use current_lsn - 1 because persist_wal_async already incremented it. */
uint64_t written_lsn;
pthread_mutex_lock(&p->lock);
written_lsn = p->current_lsn - 1;
pthread_mutex_unlock(&p->lock);
int sync_err = repl_wait_ack(&p->repl, written_lsn,
p->config.sync_timeout_ms);
if (sync_rc_out)
*sync_rc_out = rc;
return rc;
*sync_rc_out = sync_err;
/* -ETIMEDOUT = degraded mode (local-only durability); -ENODEV = no standbys.
* Both are acceptable — primary continues operating. */
return 0;
}
void persist_log_entry_metadata(const wal_entry_hdr_t* hdr)