walrus sync-standby: durable synchronous-standby WAL receiver - #18
Closed
achudnovskij wants to merge 1 commit into
Closed
walrus sync-standby: durable synchronous-standby WAL receiver#18achudnovskij wants to merge 1 commit into
achudnovskij wants to merge 1 commit into
Conversation
Add `walrus sync-standby <dir>`, the receiver half of Ubicloud sync_pair HA: it streams WAL from a primary as a durable synchronous standby, fsyncs WAL to local disk, and acks the flushed LSN to join the primary's `synchronous_standby_names = ANY 1` quorum alongside the streaming standby. Self-contained under `src/pg/sync_replica/` with its own CLI subcommand, decoupled from the Uploader receiver (`wal::receive`, unchanged in behavior). Architecture (two threads, bridged by `Shared`): - receive.rs: the synchronous (tokio-free) hot path — blocking socket I/O + positioned `write_all_at` + `fdatasync`, so a sole-acker commit pays no task-scheduler latency. Durability invariant: the flush ack never precedes the fdatasync of the bytes it covers. - controller.rs: its own tokio runtime — the sole-acker poller, the retention janitor + back-pressure, and the mTLS control API. - shared.rs: lock-free atomics (fsyncd_lsn, sole_acker, ack_ceiling) + Notify. Capabilities: - mTLS control API (api.rs): GET /v1/status, POST /v1/dr-catchup, POST /v1/failover-primary — the endpoints the Ubicloud CP drives on failover. - Sole-acker detection: the poller reads the peer standby's flush_lsn from pg_stat_replication; when the standby is the pacing acker (2-acker) the receiver coalesces fsyncs behind a ~1ms batch window (~4x lower IOPS at no commit-latency cost); when it's the only acker it fsyncs per-frame for minimum latency. - dr-catchup (dr_tail.rs): on failover, upload the retained receiver-only WAL tail to a DR-tail S3 lane so a promotion candidate can fetch + replay it, with a contiguous-durable gate + from-anchor so RPO=0 holds through standby-behind / both-down / total-loss. - Retention janitor: prune segments the primary has archived; back-pressure pins the ack ceiling (with a hard cap) so retained WAL can't grow unbounded. - Reconnect/retarget loop: a stream break or failover-primary ends the session, not the process — the control API stays up and the receiver resumes from its durable frontier on the (possibly new) primary. Also: docs/ with mermaid diagrams (architecture, state machines, WAL record + segment lifecycles, side-channel queries); the docker/ operator image + Rust Dockerfile + entrypoint that execs sync-standby. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
walrus sync-standby <dir>— the receiver half of Ubicloudsync_pairHA: it streams WAL from a primary as a durable synchronous standby, fsyncs WAL to local disk, and acks the flushed LSN to join the primary'ssynchronous_standby_names = ANY 1quorum alongside the streaming standby. Self-contained undersrc/pg/sync_replica/with its own CLI subcommand, decoupled from the Uploader receiver (wal::receive).Draft — opening for review; end-to-end re-validation on the rebased code is in progress (see Testing).
Architecture (two threads, bridged by
Shared)receive.rs— the synchronous (tokio-free) hot path: blocking socket I/O + positionedwrite_all_at+fdatasync, so a sole-acker commit pays no task-scheduler latency. Durability invariant: the flush ack never precedes thefdatasyncof the bytes it covers.controller.rs— its own tokio runtime: the sole-acker poller, the retention janitor + back-pressure, and the mTLS control API.shared.rs— lock-free atomics (fsyncd_lsn,sole_acker,ack_ceiling) + aNotify.Diagrams + lifecycles in
src/pg/sync_replica/docs/(architecture, state machines, WAL record + segment lifecycles, side-channel queries).Capabilities
api.rs):GET /v1/status,POST /v1/dr-catchup,POST /v1/failover-primary— the endpoints the Ubicloud CP drives on failover.flush_lsnfrompg_stat_replication; when the standby is the pacing acker (2-acker) the receiver coalesces fsyncs behind a ~1 ms batch window (~4× lower IOPS at no commit-latency cost); when it's the only acker it fsyncs per-frame for minimum latency.dr_tail.rs): upload the retained receiver-only WAL tail to a DR-tail S3 lane so a promotion candidate can replay it (contiguous-durable gate + from-anchor → RPO=0 across standby-behind / both-down / total-loss).failover-primaryends the session, not the process — the control API stays up and the receiver resumes from its durable frontier).Deployment
docker/wal-receive/entrypoint.shexecswalrus sync-standby(the operator uses the imageENTRYPOINT, no operator command change). Requires the primary to grant the replication rolepg_read_all_statsso the sole-acker poller can readflush_lsn(done in the Ubicloud repo:post-installation-script+ the receiver nexus).Review notes (rebase reconciliation)
This branch was squashed to one commit and rebased on
main, which independently reworked config (Vars+ json) + TLS (TlsParams) + thehandlesignature. Reconciliation choices:wal/receive.rsis taken verbatim frommain(the Uploader is untouched by this feature).Vars-based config: the CLI resolvesPgConfig::resolve(&vars)+slot_name(&vars)and passes them tosync_replica::run(mirrorswal-receive);build_dr_s3_storageusess3_config(&Vars::default(), …).maybe_upgrade_syncnow threadsTlsParams(matches main's asyncmaybe_upgrade).query_wal_segment_sizeexists both inconn.rs(forsync_replica) and privately inwal/receive.rs(main's Uploader) — a candidate follow-up to unify.Testing
cargo fmt/cargo clippy --all-targets -D warnings/cargo test— 497 lib tests pass, clippy clean, on the rebased commit.🤖 Generated with Claude Code