tidaldb/tidal-net/proto/wal_shipping.proto
jx12n 5ed2edb211 feat(m11): quorum-acked writes — ack=leader|quorum, commit index, durable frontier reports (m11p3)
ack=quorum gates replicated writes on a majority of the replica set durably
holding them: followers push their durably-applied frontier (ReportApplied,
once per apply round, decoupled from ship acks), the leader folds frontier
reports + ship-ack hints + heal resumes into a leadership-scoped CommitIndex
(k-th-largest durable mark), and handlers await it through an async
watch-channel bridge (zero parked threads per waiter). Honest timeouts:
retryable 503 naming the laggards; x-tidal-seq on every cluster write.
Follower blob applies are batched under group-commit fsyncs (22x seeding).
Exit gate: 167/167 leader-SIGKILL kill points, zero acked-write loss.

Seven-dimension review pass (all confirmed findings fixed):
- WAL blob drain now ABORTS on the first write failure instead of reusing
  the failed seqno mid-drain (a torn record buried mid-segment would
  truncate every later acked record on replay)
- apply_replicated_blobs waits every staged append even after a mid-batch
  failure, parses metadata once, and moves records into Arcs shared with
  the WAL writer (no deep clone per record on the follower apply path)
- CommitIndex: zero-peer fast path now respects demotion (active checked
  under lock before the single-replica return), k-th-largest uses
  select_nth over a reused scratch buffer
- await_quorum: re-reads the index once after the deadline fires (no false
  503 for a write that committed in the race window), warns when the
  commit-watch bridge dies outside shutdown, zero-peer path checks active
- notify_applied report failures: WARN on the first failure of a streak,
  INFO on recovery (a silently stalling frontier reads as unexplained
  quorum 503s); receiver skips re-notifying unadvanced frontiers
- x-tidal-deduplicated: 1 marks dedup-suppressed signal writes (relayed
  through forwards) so durability cursors can tell dedup from no-seqno
- docs: 167/167 kill-point record corrected in CHANGELOG; rolling-upgrade
  order (leader first — a pre-m11p3 leader silently downgrades quorum
  requests to leader-ack) in CHANGELOG + runbook §8; monitoring note for
  report-loss diagnosis on the quorum-timeout alert

Verified: workspace clippy -D warnings (incl. cluster-e2e targets), full
tidaldb/tidal-net/tidal-server/tidalctl suites green, tier-3 multi-process
quorum suite green (8/8 kill points, zero acked loss, partition gate/recover).
2026-06-11 13:28:08 -06:00

98 lines
3.4 KiB
Protocol Buffer

syntax = "proto3";
package tidal.replication.v1;
// Globally unique identifier for a WAL segment.
message WalSegmentId {
uint32 region_id = 1;
uint32 shard_id = 2;
uint64 seqno = 3;
}
// A WAL segment ready for shipping to a peer shard.
message ShipSegmentRequest {
WalSegmentId id = 1;
bytes payload = 2;
uint64 event_count = 3;
// The segment's authoritative last WAL sequence number, computed by the
// leader from the ORIGINAL (pre-community-overlay-filter) bytes. Lets the
// receiver advance its replication-lag leader high-water-mark even for an
// all-local segment that filters to an empty payload (obs-REPL-1). A 0 value
// (e.g. from an older sender that omits this field) means "unknown" and the
// receiver falls back to the per-batch boundaries it decodes.
uint64 leader_last_seq = 4;
// The shipping stream's baseline (m11p2): the WAL seqno at which this
// leader's stream STARTED. Non-zero only on catch-up stream chunks from a
// promoted leader; seqnos at or below it are pre-stream history the
// receiver jumps its frontier past instead of treating as a gap. 0 (the
// default, and what every live unary ship carries) means "stream from the
// beginning".
uint64 stream_baseline = 5;
}
// Response to a segment shipment.
message ShipSegmentResponse {
bool accepted = 1;
// The receiver's contiguous applied seqno for the request's source shard at
// acceptance time (m11p2): a monotonic hint the sender folds into its acked
// frontier so retries of already-applied data prune and heal needs no
// separate status fetch. 0 = unknown (older peer / no applied source wired).
uint64 applied_seqno = 2;
}
// Request to stream segments from a given sequence number.
message StreamRequest {
uint32 shard_id = 1;
uint64 from_seqno = 2;
}
// Heartbeat request matching ControlPlane's ShardStats.
message HeartbeatRequest {
uint32 shard_id = 1;
uint32 region_id = 2;
uint64 entity_count = 3;
double signal_throughput_eps = 4;
uint64 disk_bytes = 5;
// Replication lag per peer region (region_id -> lag in events).
map<uint32, uint64> replication_lag = 6;
uint64 last_heartbeat_ns = 7;
}
// Heartbeat acknowledgement.
message HeartbeatResponse {
bool acknowledged = 1;
}
// A follower's self-report of its durable frontier (m11p3).
//
// Pushed by the receiver once per apply round — fully decoupled from ship
// acks, so the leader's quorum commit index stays fresh even when its
// outbound ships stall (gap-parked follower, quiet leader, pull catch-up).
message AppliedReport {
// The reporting node's shard id.
uint32 reporter_shard = 1;
// The stream's source shard (the leader being reported to).
uint32 source_shard = 2;
// The reporter's contiguous durably-applied seqno for that stream.
uint64 applied_seqno = 3;
}
// Applied-report acknowledgement.
message AppliedReportAck {
bool acknowledged = 1;
}
// WAL segment shipping service between tidalDB shards.
service WalShipping {
// Ship a single WAL segment to a peer shard (unary).
rpc ShipSegment(ShipSegmentRequest) returns (ShipSegmentResponse);
// Stream WAL segments from a given sequence number (server-streaming).
rpc StreamSegments(StreamRequest) returns (stream ShipSegmentRequest);
// Periodic health check for the ControlPlane.
rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);
// Follower -> leader durable-frontier report (m11p3 quorum acks).
rpc ReportApplied(AppliedReport) returns (AppliedReportAck);
}