Skip to content

feat(replication): WS plane — shard-0-pinned deterministic WS.CREATE/DROP + live streaming + PSYNC registry blob (Wave B)#293

Merged
pilotspacex-byte merged 9 commits into
mainfrom
feat/wave-b-ws-plane
Jul 12, 2026
Merged

feat(replication): WS plane — shard-0-pinned deterministic WS.CREATE/DROP + live streaming + PSYNC registry blob (Wave B)#293
pilotspacex-byte merged 9 commits into
mainfrom
feat/wave-b-ws-plane

Conversation

@pilotspacex-byte

@pilotspacex-byte pilotspacex-byte commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Wave B WS plane (task #34): the process-global workspace registry is now deterministic, replicated, and snapshot-covered. Before this, WS.CREATE/DROP mutated the registry from any conn thread, minted nondeterministic UUIDv7s, never streamed to replicas, and had no FULLRESYNC coverage — a replica's registry was permanently empty (and, until PR #292, silently mutable by clients).

Design

  • Shard-0 affinity hop: the whole mutation + WorkspaceCreate/Drop WAL record + replication push runs in ONE synchronous stretch on shard 0's thread (ShardMessage::WsRegistryCreate/Drop, mirroring the WsDropCleanup hop). Zero .await inside the write arm and the PrepareReplicaSync arm → a WS.CREATE racing a snapshot lands in exactly one of {blob, stream} (R2 offset-cut lesson; verified by line-trace in adversarial review).
  • Live stream: internal WS.CREATE.APPLY/WS.DROP.APPLY pseudo-commands carry the exact WAL-encoder bytes; recognized only in apply_local (unreachable from client dispatch — traced both runtimes; not in any phf table). Replica applies the MASTER's ws_id + created_at verbatim (from_bytes, never new_v7).
  • FULLRESYNC: singular shard-0-authoritative MOON_AUX_WORKSPACE_REGISTRY blob (versioned, bounds-checked decode, new fuzz target ws_registry_record registered in both CI fuzz matrices); install is authoritative-replace; multi-shard PSYNC aborts if shard 0's leg fails rather than proceeding blobless.
  • warn_unreplicated_plane split per-plane; WS half retired.
  • Replica apply is in-memory only, matching the graph plane's model (restarted replica resyncs from master); making plane applies WAL-persistent on replicas is a deliberate cross-plane follow-up, not WS-only divergence.

Adversarial review (independent agent): SHIP-WITH-FIXES — both findings fixed in-branch

  • P0: WS.CREATE's hop leg discarded send/reply outcomes and unconditionally returned the ws_id — a wedged ring or 30s reply timeout yielded a phantom workspace. Now fail-closed with a process-global-registry probe disambiguating slow-reply from never-executed.
  • P1: WS.DROP hop timeout resolved removed=false even when shard 0 completed the drop, permanently skipping the {ws_hex}: key sweep (unretryable leak — the registry entry is already gone). Same registry probe: entry gone → run the (idempotent) sweep.
  • Reviewer confirmed correct: offset-cut atomicity both PSYNC paths, no self-hop deadlock, pseudo-command unforgeable by clients, verbatim UUID apply, authoritative blob install, cleanup parity, both feature matrices.

Verification

Follow-up PR (last Wave B piece): MQ plane replication (stage 2b), already built and under review.

Summary by CodeRabbit

  • New Features

    • Added replication for workspace creation and deletion, including live updates and snapshot backfill.
    • Preserved workspace identifiers and creation timestamps consistently across replicas.
    • Added shard-aware handling to ensure workspace operations remain consistent across connections.
  • Bug Fixes

    • Improved cleanup when workspaces are dropped.
    • Clarified replication warnings for non-replicated message-queue commands.
  • Tests

    • Added integration coverage for live replication, snapshot synchronization, workspace deletion, and sharded operation consistency.
    • Added fuzz testing for malformed workspace synchronization data.

Wave B ws-plane groundwork. Adds `workspace::repl` — thin RESP-array
wrapping of the existing WAL payload bytes
(`encode_workspace_create`/`encode_workspace_drop`) into two internal
pseudo-commands, `WS.CREATE.APPLY` / `WS.DROP.APPLY`, so the exact bytes
already written to shard 0's WAL can flow over the replication wire and be
recognized by the replica apply path before generic dispatch (mirrors
`GRAPH.ADDNODE` and friends). No new encoding is invented: the payload is
always the verbatim output of the WAL encoders.

Not yet wired into any write path or replica apply arm — that lands in a
follow-up commit alongside the shard-0 affinity hop.

author: Tin Dang
Wave B ws-plane groundwork. Adds `replication::ws_sync` — export/install of
the process-global `WorkspaceRegistry` as a new `MOON_AUX_WORKSPACE_REGISTRY`
RDB aux blob (`persistence::redis_rdb`), for the FULLRESYNC full-resync leg.

Unlike the per-shard `MOON_AUX_GRAPH_STORE`/vector/text blobs, the registry
is a single process-global structure (one `Mutex` on `ShardDatabases`,
visible from every shard's thread) rather than sharded state, so this blob
is singular and shard-0-authoritative: exactly one blob belongs in the
merged RDB regardless of `--shards`. Versioned little-endian format
(version byte + entry count + per-entry ws_id/name/created_at), bounds-
checked decoder returning `None` on any malformed input (truncated header,
truncated entry, unknown version) rather than panicking — a corrupted or
adversarial PSYNC stream must never crash a replica. `None` and `Some(empty)`
registries encode identically so a replica can distinguish "master
authoritatively has zero workspaces" from "pre-WS-sync master, aux absent
entirely" once wired into `load_snapshot`.

Includes 5 unit tests (round-trip, none/empty equivalence, 4 malformed-input
shapes, truncated-full-blob, empty-name/negative-created_at edge case) and a
new `ws_registry_record` cargo-fuzz target for the decoder (Testing rule:
every new decoder needs a fuzz target).

Not yet wired into any PSYNC capture site or `load_snapshot` — that lands in
a follow-up commit.

author: Tin Dang
Wave B ws-plane. Adds the cross-shard hop that will let any connection
thread route the process-global WorkspaceRegistry mutation onto shard 0's
own OS thread, mirroring the existing `WsDropCleanup` hop shape.

New `ShardMessage::WsRegistryCreate(Box<WsRegistryCreatePayload>)` /
`WsRegistryDrop` variants (boxed vs inline sized per the existing 64-byte
enum budget) and their `handle_shard_message_shared` arms run the WHOLE
mutation + WAL-append + replication-record sequence in one synchronous
stretch on shard 0's thread — this is what keeps the replication offset
advance atomic with the mutation w.r.t. a concurrent PSYNC snapshot capture,
the same argument `record_local_write` relies on for per-shard state,
applied here to the one piece of state that is global. A new
`record_ws_registry_write` helper duplicates (rather than shares)
`record_local_write`'s backlog-append + offset-advance + self-queue-push
steps, because this call site has no `ConnectionContext` — only the raw
per-shard pieces already threaded through `handle_shard_message_shared`'s
signature (same precedent as `wal_append_and_fanout` being an independent,
not shared, copy of the same steps).

Also wires the WS registry export into `PrepareReplicaSync`'s reply
(`PreparedShardSync::ws_registry_blob`, `Some` only on shard 0's leg, `None`
elsewhere) so the master-side PSYNC capture sites can pick it up in a
follow-up commit.

Not yet reachable from any command handler — `WsRegistryCreate`/`Drop` are
constructed nowhere yet; that lands with the write-path wiring.

author: Tin Dang
Wave B ws-plane. Threads `Arc<ShardDatabases>` through the replica's apply
path so it can reach the process-global `WorkspaceRegistry`, which lives on
`ShardDatabases` rather than the thread-local `ShardSlice`:

- `apply::apply_local` recognizes the `WS.CREATE.APPLY` / `WS.DROP.APPLY`
  internal pseudo-commands (`workspace::repl`) before generic dispatch.
  `apply_ws_create` installs the MASTER's `ws_id` + `created_at` VERBATIM —
  UUIDv7 is nondeterministic, so a replica must never mint its own id.
  `apply_ws_drop` removes the entry and reruns the master's best-effort
  `{ws_hex}:`-prefix key sweep across every db on this shard (R0 replication
  is single-shard-only, so sweeping the one shard IS the whole sweep — no
  cross-shard hop to mirror). Both are in-memory only, matching
  `apply_graph`'s no-local-WAL-persistence model: a restarted replica
  resyncs the registry from its master rather than replaying a local copy.
- `apply::load_snapshot` reads the (singular, shard-0-authoritative)
  `MOON_AUX_WORKSPACE_REGISTRY` aux blob and installs it authoritatively
  after a successful keyspace load — same convention as the graph store
  (empty blob = master authoritatively has none, replaces local state;
  absent aux = pre-WS-sync master, warn-and-keep-local).
- `ReplicaTaskConfig` gains a `shard_databases` field (both runtime
  variants' 4 `apply_local`/`load_snapshot` call sites updated); its two
  construction sites (`handler_monoio`/`handler_sharded` `dispatch.rs`)
  now pass `ctx.shard_databases.clone()`.

Not yet reachable — the master-side PSYNC capture and the write-path shard-0
hop wiring land in the next two commits.

author: Tin Dang
Wave B ws-plane. Wires `replication::ws_sync::export_workspace_registry`
into the two FULLRESYNC capture sites:

- `handle_psync_inline_single_shard`: shard 0 IS this thread's shard, so the
  capture is trivially in the same synchronous stretch as the offset read;
  always written to `moon_aux` (empty blob tells the replica the master
  authoritatively has zero workspaces), same convention as the graph blob.
- `handle_psync_inline_multi_shard`: only shard 0's `PreparedShardSync` leg
  populates `ws_registry_blob` (every other shard replies `None`, wired in
  the previous commit); the merge loop keeps the first `Some`, matching the
  `vector_defs`/`text_defs` convention.

Combined with the previous commit's `load_snapshot` install, a replica
attaching to a master that already has workspaces now backfills them from
the snapshot leg.

author: Tin Dang
Wave B ws-plane — the write-path wiring that makes everything from the
previous five commits reachable.

`try_handle_ws_command` (monoio) now branches on `ctx.shard_id == 0`: a
connection already on shard 0 runs the mutation + WAL-append + replication-
record sequence inline (as before, just now also emitting the live
replication record); every other connection sends `WsRegistryCreate` /
`WsRegistryDrop` to shard 0 via `spsc_send` and awaits the bounded oneshot
reply — same dual-path shape `WsDropCleanup` already used for the key-sweep
half of WS.DROP, now applied to the registry mutation itself. Command
semantics (return value, error cases) are unchanged; only the mutation's
execution thread and its now-replicated side effect are new.

Live replication emission happens only on the inline (shard-0) branch, gated
on `replication_fanout_active(ctx)` exactly like the graph plane: the
WorkspaceCreate/WorkspaceDrop WAL payload is wrapped via
`workspace::repl::serialize_ws_create_apply`/`serialize_ws_drop_apply` and
pushed with `record_local_write_db`. The foreign-hop branch's equivalent
push happens on shard 0's own thread inside `handle_shard_message_shared`
(landed in commit 3) — so every WS.CREATE/DROP is replicated exactly once
regardless of which connection thread received it.

Also splits the former combined `warn_unreplicated_plane` fail-loud marker
(`handler_monoio/ft.rs`) into `warn_mq_unreplicated`: the WS half is deleted
now that this plane replicates; the MQ half survives (MQ.* is not yet
wired into replication — separate branch/task).

author: Tin Dang
Wave B ws-plane. New `tests/replication_ws.rs`, self-contained (own
copies of `start_moon`/`Killer`/`send_cmd`/`send_resp`/`wait_until`,
modeled on `tests/replication_graph.rs`):

- `replica_syncs_ws_create_live_stream` — replica attaches to an EMPTY
  master, then WS.CREATE runs; asserts the replica resolves the MASTER's
  exact `ws_id` via WS.INFO/WS.AUTH (the verbatim-apply / no-re-mint
  correctness guard).
- `replica_backfills_ws_registry_from_snapshot` — WS.CREATE runs BEFORE the
  replica ever attaches; asserts FULLRESYNC backfill produces an identical
  WS.INFO on both sides, and that a live write past the snapshot boundary
  still replicates.
- `replica_syncs_ws_drop` — WS.DROP with a replica attached; asserts the
  workspace disappears from WS.LIST and WS.AUTH fails identically on both
  sides.
- `ws_create_shard0_hop_consistent_across_shards` — `--shards 4`, no
  replica: WS.CREATE followed by 16 connections' WS.INFO/WS.LIST, exercising
  the shard-0 hop from whichever shard SO_REUSEPORT lands each connection on.

Also adds the `[Unreleased]` CHANGELOG entry summarizing the six-commit
Wave B ws-plane series.

Verified: all 4 new tests green against a `release-fast` build; existing
`replication_graph` (3), `replication_planes` (11), and `shardslice_live`
(6, including both `test_ssm3_ws_*` legs) stay green — no regressions.
Both default and `runtime-tokio,jemalloc` clippy matrices clean at
`-D warnings`; `cargo fmt --check` clean; full `cargo test --profile
release-fast --lib` green (4156 passed, 1 ignored, 0 failed).

author: Tin Dang
…overy

Adversarial review of the ws-plane branch (verdict SHIP-WITH-FIXES):

P0: the WS.CREATE shard-0 hop discarded both the spsc_send and
recv_reply_bounded outcomes and unconditionally replied with the ws_id —
a wedged ring or 30s reply timeout returned a phantom id the registry
never saw (WS AUTH/INFO against it 404s), directly contradicting the
sibling WS.DROP path that already gated on the reply. Fixed: the hop
outcome is checked, and on failure the process-global registry is probed
to disambiguate "shard 0 executed but the reply was slow/lost" (entry
present, success) from "the mutation never happened" (entry absent,
fail closed with a retryable error).

P1: a WS.DROP hop timeout resolved `removed = false` even when shard 0
completed the drop (timeout means "no reply in 30s", not "failed" — the
handler's synchronous O(keys × databases) WsDropCleanup scan makes slow
replies plausible). The registry entry being gone meant no retry could
ever re-trigger the {ws_hex}: key-prefix sweep — the workspace's keys
leaked permanently. Fixed with the same registry probe: entry gone →
the drop landed, run the (idempotent) sweep; entry present → genuine
hop failure, fall through to the not-found path as before.

Both fixes are conn-thread-only; the shard-0 handler and replication
paths are untouched. Gates re-run green: lib 4156/4156, replication_ws
4/4 (incl. the 16-iteration cross-shard hop consistency test), clippy
both matrices, fmt.

author: Tin Dang
The ws-plane branch added the fuzz target binary but not its CI matrix
entries (Testing rule: every new decoder fuzz target runs in CI).

author: Tin Dang
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Wave B replication for the workspace registry. WS.CREATE and WS.DROP now route mutations through shard 0, replicate via WAL and live fanout, transfer registry state in full-resync snapshots, and apply deterministic workspace metadata on replicas.

Changes

Workspace registry replication

Layer / File(s) Summary
Replication wire formats and codecs
src/replication/ws_sync.rs, src/workspace/repl.rs, fuzz/*, .github/workflows/fuzz.yml
Adds workspace-registry snapshot encoding, WS apply command serialization, decoding tests, and fuzz coverage.
Shard-0 mutation routing
src/server/conn/handler_monoio/write.rs, src/shard/dispatch.rs, src/shard/spsc_handler.rs, src/server/conn/handler_monoio/ft.rs
Routes WS.CREATE and WS.DROP through shard 0 and synchronizes registry, WAL, replication backlog, offsets, and live fanout.
Full-resync snapshot export
src/replication/master.rs, src/shard/spsc_handler.rs, src/shard/dispatch.rs, src/persistence/redis_rdb.rs
Adds a process-global workspace-registry AUX blob to single-shard and merged multi-shard RDB snapshots.
Replica application and shared-state wiring
src/replication/apply.rs, src/replication/replica.rs, src/server/conn/handler_*/dispatch.rs
Passes ShardDatabases into replica paths, applies WS.CREATE.APPLY and WS.DROP.APPLY, installs snapshot state, and removes dropped-workspace keys.
Integration validation and release support
tests/replication_ws.rs, CHANGELOG.md
Adds ignored tests for live replication, snapshot backfill, drop propagation, and shard-0 consistency, with corresponding changelog documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WSCommandHandler
  participant ShardZero
  participant ReplicationBacklog
  participant Replica
  Client->>WSCommandHandler: WS.CREATE or WS.DROP
  WSCommandHandler->>ShardZero: route registry mutation
  ShardZero->>ReplicationBacklog: append WAL payload and advance offset
  ReplicationBacklog->>Replica: deliver WS apply command
  Replica->>Replica: install workspace metadata or remove workspace keys
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific, concise, and matches the main change: deterministic WS.CREATE/DROP replication with live streaming and PSYNC registry blob support.
Description check ✅ Passed The description is substantive and covers the summary, implementation design, and verification, though it uses custom headings instead of the template's exact sections.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/wave-b-ws-plane

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/server/conn/handler_monoio/write.rs (1)

98-106: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Confirm the inline vs. shard-0-hop replication paths are intentionally divergent.

The same logical WS.CREATE/DROP produces a different replication record depending on which shard the client connection landed on:

  • Inline (shard 0 connection): record_local_write_db(ctx, conn.selected_db, ...) — in a multi-shard master this fuses a SELECT <selected_db> prefix ahead of the (db-agnostic) WS.*.APPLY record, and is gated on replication_fanout_active(ctx).
  • Hop (non-shard-0 connection): the WsRegistryCreate/WsRegistryDrop arm in src/shard/spsc_handler.rs calls record_ws_registry_write, which emits no SELECT prefix and is gated only on fanout_hint_active().

Two consequences worth verifying: (1) a spurious SELECT on the inline path for a db-agnostic record, and (2) the differing fanout gate means the master replication offset can advance under different conditions for the two paths. Since these encode the same operation, aligning both paths (prefer the db-agnostic record_ws_registry_write shape and one shared gate) avoids stream/offset divergence between otherwise-identical clients.

Also applies to: 177-186

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/conn/handler_monoio/write.rs` around lines 98 - 106, Align the
inline WS.CREATE/DROP replication handling in the write path with the
shard-0-hop handling: use the same db-agnostic registry-write recording
mechanism as the WsRegistryCreate/WsRegistryDrop arms and the same fanout gate
as the hop path. Update both highlighted inline branches, replacing
record_local_write_db and its replication_fanout_active check while preserving
the existing serialized operation payload.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/server/conn/handler_monoio/write.rs`:
- Around line 98-106: Align the inline WS.CREATE/DROP replication handling in
the write path with the shard-0-hop handling: use the same db-agnostic
registry-write recording mechanism as the WsRegistryCreate/WsRegistryDrop arms
and the same fanout gate as the hop path. Update both highlighted inline
branches, replacing record_local_write_db and its replication_fanout_active
check while preserving the existing serialized operation payload.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b5b106b-6566-4076-9766-17bb69bc6e46

📥 Commits

Reviewing files that changed from the base of the PR and between c0b050f and fc023ac.

📒 Files selected for processing (19)
  • .github/workflows/fuzz.yml
  • CHANGELOG.md
  • fuzz/Cargo.toml
  • fuzz/fuzz_targets/ws_registry_record.rs
  • src/persistence/redis_rdb.rs
  • src/replication/apply.rs
  • src/replication/master.rs
  • src/replication/mod.rs
  • src/replication/replica.rs
  • src/replication/ws_sync.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/handler_monoio/ft.rs
  • src/server/conn/handler_monoio/write.rs
  • src/server/conn/handler_sharded/dispatch.rs
  • src/shard/dispatch.rs
  • src/shard/spsc_handler.rs
  • src/workspace/mod.rs
  • src/workspace/repl.rs
  • tests/replication_ws.rs

@pilotspacex-byte pilotspacex-byte merged commit 72021d9 into main Jul 12, 2026
11 checks passed
@pilotspacex-byte pilotspacex-byte deleted the feat/wave-b-ws-plane branch July 12, 2026 05:33
pilotspacex-byte added a commit that referenced this pull request Jul 12, 2026
… Stream codec data-loss fix (Wave B stage 2b, task #34) (#294)

* fix(persistence): PSYNC RDB Stream codec discarded all content (data loss)

The FULLRESYNC RDB codec (persistence::redis_rdb, distinct from the
persistence::rdb SAVE/BGSAVE codec) serialized Stream values as a
placeholder "__stream__:<len>" string — entries, last_id, consumer
groups, PEL state, durable flag, and max_delivery_count were all
silently discarded on every replica full resync. A durable MQ queue
snapshotted this way would come back empty on the replica.

Add a full RDB_TYPE_STREAM_MOON (0xC8) codec, ported from
persistence::rdb's battle-tested Stream serializer: entries, last_id,
consumer groups (PEL + per-consumer pending), durable flag, and
max_delivery_count round-trip losslessly. Every new count field is
bounds-checked via check_alloc_bound before allocating, matching the
existing DoS-avoidance discipline in this module.

Also adds the MOON_AUX_MQ_REGISTRY aux-field constant used by the
Wave B stage 2b MQ replication work landing in subsequent commits.

author: Tin Dang

* feat(mq): replication wire-framing constants + generalized apply engine

Preparatory plumbing for Wave B stage 2b MQ replication:

- src/mq/wal.rs: five synthetic MQ._REPL.CREATE/PUSH/POP/ACK/TRIGGER
  pseudo-command names plus is_mq_replay_command(), used to frame the
  existing encode_mq_*/decode_mq_* WAL payload bytes on the wire and to
  route them on the replica side (never dispatched to real clients).

- src/mq/trigger.rs: TriggerRegistry::iter(), needed to snapshot every
  registered trigger for the FULLRESYNC registry blob landing in a
  later commit.

- src/shard/shared_databases.rs: generalize apply_mq_create/push/pop/
  ack/trigger over a new MqApplyTarget trait (mq_databases_mut() /
  mq_durable_queue_registry_mut() / mq_trigger_registry_mut()),
  implemented for both ShardSliceInit (boot-time WAL replay) and
  ShardSlice (live replica apply, added next). One apply engine now
  serves both boot recovery and replica replication instead of two.

author: Tin Dang

* feat(replication): MQ replica-side apply engine + FULLRESYNC snapshot leg

Wave B stage 2b, replica-consuming half. Two pieces:

1. Live-record apply (src/replication/apply.rs): apply_local routes any
   MQ._REPL.* record to a new apply_mq(), which decodes the single
   bulk-string payload with the existing decode_mq_* functions and
   dispatches to the SAME apply_mq_* engine (shard::shared_databases,
   generalized last commit) boot-time WAL replay uses. A replica never
   fires MQ.TRIGGER callbacks on apply — registrations are stored as
   opaque data, matching boot-time replay.

2. FULLRESYNC snapshot leg (src/replication/mq_sync.rs, new): a
   per-shard MOON_AUX_MQ_REGISTRY blob carries the durable-queue
   registry and trigger registry — shard-level bookkeeping that lives
   outside the keyspace (Stream CONTENT rides the ordinary RDB body,
   covered by the redis_rdb.rs Stream codec fix). Wired at both master
   capture sites (single-shard PSYNC in master.rs, and the merged
   multi-shard path via a new PreparedShardSync.mq_blob field populated
   in spsc_handler.rs) and installed additively into every replica
   shard by load_snapshot (apply.rs), mirroring
   graph_sync::install_graph_store_many's precedent exactly: an EMPTY
   blob list (pre-MQ-replication master) warns-and-keeps local state; a
   non-empty list is authoritative and replaces it.

author: Tin Dang

* feat(replication): MQ master-side live-stream emission (Wave B stage 2b)

Emits MQ.* effect records to attached replicas at the moment they're
durably WAL-logged on the owner shard, closing the plane gap
warn_unreplicated_plane previously fail-loud-warned about for MQ.*
(the WS.* half of that warning is unaffected — a separate agent tracks
retiring it once WS replicates).

- src/replication/state.rs: record_local_write_global /
  record_local_write_db_global — ctx-free counterparts of
  record_local_write(_db) that resolve the same per-process
  ReplicationState Arc via admin::metrics_setup::get_global_repl_state_arc(),
  needed because shard::mq_exec runs on the shard's own OS thread
  without a ConnectionContext in scope. Also adds
  ReplicationState::num_shards().

- src/shard/mq_exec.rs: replicate_mq_record(), called from all five
  owner-shard handlers (CREATE/PUSH/POP/ACK/TRIGGER) right where each
  already builds its WAL payload — ships the IDENTICAL encoded bytes as
  one of the MQ._REPL.* pseudo-commands, gated single-shard-only
  (num_shards == 1), the same posture graph's own live path takes for
  its multi-shard gap. monoio-only; a no-op under runtime-tokio.

- src/server/conn/handler_monoio/txn.rs: MQ.PUBLISH's TXN-materialization
  leg replicates the same way at commit, on the connection's own thread
  (which — since owner == ctx.shard_id in this leg — IS the owning
  shard's thread).

- src/server/conn/handler_monoio/write.rs: retire the MQ half of
  warn_unreplicated_plane's call site now that MQ replicates in the
  single-shard configuration; the WS.CREATE/DROP call site is untouched.

A multi-shard master still does not live-stream MQ writes (durability
is unaffected — WAL + FULLRESYNC still cover it); tracked as a known
follow-up alongside graph's own multi-shard live-stream gap.

author: Tin Dang

* test(mq): replication_mq.rs integration suite + 2 new fuzz targets

tests/replication_mq.rs, modeled on tests/replication_graph.rs +
tests/replication_planes.rs (single-shard master scope, matching
graph's own precedent):

- replica_syncs_mq_live_stream: MQ.CREATE/PUSH/ACK on the master reach
  an already-attached replica, verified via MQ.POP on the replica side
  (proves both registry state and message content replicated, not just
  the raw key) plus a stream-health assertion (zero reconnects).
- replica_backfills_mq_from_snapshot: a replica attaching to a master
  that already has MQ state backfills it from PSYNC FULLRESYNC (the
  redis_rdb.rs Stream codec fix is what makes this pass), then a live
  write after the snapshot boundary still lands.
- multi_shard_live_mq_write_not_streamed: pins the documented
  num_shards==1 live-stream gate down with an explicit RED-if-lifted
  test rather than leaving the limitation untested.

Two new cargo-fuzz targets (wired into both the PR and nightly
matrices in .github/workflows/fuzz.yml, per the project's "any new
parser/decoder needs a fuzz target" rule):

- mq_registry_blob: fuzzes install_mq_registry_many (the new
  MOON_AUX_MQ_REGISTRY blob decoder) on a throwaway per-iteration
  ShardSlice, mirroring mq_wal_record.rs's coverage of the sibling WAL
  op-blob decoders.
- redis_rdb_load: fuzzes persistence::redis_rdb::load_rdb directly
  (the FULLRESYNC RDB loader, including the new RDB_TYPE_STREAM_MOON
  tag) — distinct from the existing rdb_load.rs target, which covers
  the separate persistence::rdb SAVE/BGSAVE codec.

author: Tin Dang

* docs(changelog): MQ-plane replication entry (Wave B stage 2b)

author: Tin Dang

* docs(changelog): mixed-version FULLRESYNC operator note for Stream codec fix

Adversarial review of the mq-repl branch (verdict SHIP, P2 finding):
the RDB_TYPE_STREAM_MOON fix changes the PSYNC wire format for durable
Streams, so mixed-version pairs misbehave in both directions — old
replica vs new master hard-fails FULLRESYNC (reconnect-loops until
upgraded), new replica vs old master absorbs the legacy placeholder as
a corrupted string. Document the upgrade-together requirement where
operators will see it.

author: Tin Dang

* docs: retire warn_mq_unreplicated remnants after WS+MQ plane composition

Rebase-composition tidy: with both the WS plane (PR #293) and this
branch's MQ live stream shipped, the round-2 warn_unreplicated_plane
fail-loud marker has no surviving half — the renamed
warn_mq_unreplicated lost its last call site during the rebase, so the
function is removed (dead code under -D warnings) and the doc comment
in command/mq.rs plus the WS-plane CHANGELOG entry that still claimed
"warn_mq_unreplicated survives for MQ" are corrected.

author: Tin Dang

* test(mq): replica observation via reads + failover-promotion proof

Rebase-composition fix: the replication_mq suite verified replica state
by issuing MQ POP against the replica, and readonly enforcement (PR
#292, merged after this branch was cut) now correctly rejects that with
-READONLY — MQ POP claims PEL entries and advances last_delivered, a
genuine write. The enforcement is right; the tests were exercising a
hole it closed.

Replica-side verification now uses read-only stream commands (XRANGE
for content, XPENDING summary for the consumer-group PEL), which also
makes the assertions STRONGER: the live-stream test now observes the
master's MqPop claim land in the replica PEL (1 pending) and the MqAck
drain it (0 pending) directly, instead of inferring both from what a
replica-local POP re-surfaced. Both tests then add the operational
failover proof — REPLICAOF NO ONE and a real MQ POP on the promoted
node — which exercises the replicated durable flag, consumer-group
state, and last_delivered cursor end-to-end the way an actual failover
would.

3/3 green locally (macOS release, --include-ignored).

author: Tin Dang

* fix(replication): clamp MQ apply db index + assert truncation rejection

Two CodeRabbit findings on PR #294:

1. apply_mq used the payload's db index unclamped — a replica configured
   with fewer logical dbs than its master would silently no-op the
   stream mutation (MqApplyTarget::mq_databases_mut().get_mut(oob)
   returns None): CREATE registered the queue but never created the
   stream, PUSH/POP/ACK were dropped. Now clamped with the same posture
   (and debug log) as the generic KV clamp in apply_local. Boot-time WAL
   replay is unaffected — a shard replays its own records, whose db
   indices are valid by construction.

2. The RDB stream truncation test discarded the decode result, proving
   only "no panic". Every strict prefix of the length-prefixed encoding
   must hit EOF, so it now asserts is_err() at every cut point.

Gates: redis_rdb unit tests 30/30, replication_mq 3/3, clippy, fmt.

author: Tin Dang

---------

Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants