Skip to content

feat(replication): v0.7 graph-plane replication — live GRAPH.* stream + PSYNC snapshot backfill#279

Merged
pilotspacex-byte merged 4 commits into
mainfrom
feat/v0.7-graph-replication
Jul 11, 2026
Merged

feat(replication): v0.7 graph-plane replication — live GRAPH.* stream + PSYNC snapshot backfill#279
pilotspacex-byte merged 4 commits into
mainfrom
feat/v0.7-graph-replication

Conversation

@pilotspacex-byte

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

Copy link
Copy Markdown
Contributor

Summary

v0.7 graph-plane replication: live GRAPH.* mutation streaming + PSYNC full-resync snapshot backfill, hardened through two adversarial review rounds.

What ships

  • Live stream: GRAPH.* mutations replicate as their deterministic, id-pinned WAL-record form (GRAPH.ADDNODE <g> <node_id> …, FNV-hashed u16 label/prop ids — label_to_id is stateless so master and replica resolve identically). Replicas apply through the same GraphReplayCollector restart recovery uses — no id re-allocation, client-cached handles survive.
  • Snapshot backfill: FULLRESYNC RDB carries a graph aux blob (format v2) covering everything a replica can't recover without a WAL: per-segment deleted-edge sidecars (CSR to_bytes writes no validity section), freeze-retained cross-tier delta edges (original ids + weight/props), and copy-up dead-shadow tombstones — installed in dependency order (floors → segments → delta edges → shadows), fully bounds-checked before any mutation.
  • Exactly-once foundation (round-1 fixes): record_local_write performs backlog append + shard-offset advance synchronously at write time — atomic w.r.t. the inline PSYNC snapshot capture; only the live try_send defers via ReplicaLiveFanout on the self queue. RegisterReplica.push_offset captured at push time. Proven for all write/attach interleavings (round-2 verified: single/concurrent PSYNC, +CONTINUE, unregister ordering).
  • MULTI/EXEC bodies replicate (round-1 P0-1): EXEC previously persisted via the AOF-only leg — committed durably on the master, never reached the replica.
  • TEMPORAL.INVALIDATE replicates (round-2 finding B): streamed as wall-clock-pinned TEMPORAL.INVALIDATE-AT <graph> <N|E> <entity_id> <wall_ms> so valid_to matches exactly on both sides.
  • Replay liveness split (round-2 finding F): node_present (AddNode dedup) vs node_alive (mutation targets) — a stray SETPROP for a tombstoned node can no longer resurrect it into the live property index.
  • Fail-loud limitation markers (round-2 finding A): WS.*/MQ.* writes are NOT replicated in v0.7 (WS.CREATE mints a fresh UUIDv7 — verbatim streaming would diverge); a one-time warn fires when such a write executes with a replica attached. Full support tracked as follow-up (task GPU: CI/CD infrastructure for CUDA builds and testing #34), with Lua-EVAL and expiry/eviction propagation in the same family.

Scope

Single-shard master (--shards 1), matching the R0/R0.5 legs — multi-shard masters ride the R2 PSYNC redesign.

Verification

  • lib 4111 green; replication_graph 3/3 (incl. new REPL-GRAPH-03 temporal e2e, red/green-proven against the reverted master leg); replication_streaming 4/4 (incl. new MULTI/EXEC double-apply canary); replication_hardening 5/5; tokio replication_test 5/5; tokio lib replication 43/43
  • fmt + clippy clean on default and runtime-tokio,jemalloc
  • Two adversarial review rounds: round 1 (2 P0, 3 P1, 1 P2 — all fixed in fa193ba), round 2 (verified all round-1 fixes correct; findings B/E/F/G fixed in 2ba72c7, A tracked)

Summary by CodeRabbit

  • New Features

    • Added graph replication for live updates, snapshots, node properties, edges, and temporal invalidation.
    • Added replication support for transactional MULTI/EXEC writes.
    • Graph queries that are read-only can now run on replicas.
  • Bug Fixes

    • Improved replication reliability, exactly-once behavior, snapshot consistency, and stream health.
    • Prevented deleted graph nodes from being unintentionally restored.
    • Added warnings for write operations that are not replicated in v0.7.
  • Documentation

    • Updated the unreleased changelog with replication improvements and known limitations.

… local-write fanout

The SPSC mesh is N·(N−1) with skip-self mapping, so a task on a shard's
own thread had NO producer to that shard. At --shards 1 the producer Vec
is EMPTY: the inline PSYNC task's RegisterReplica failed every attach
("shard 0 producer missing") and the replica fell into a 0.5s
reconnect/full-resync loop. R0/R0.5 tests stayed green because each
resync's RDB carried the latest keyspace + FT defs — data crawled across
via snapshot polling, masking the dead live stream entirely.

Repair, in four parts:

1. shard::self_msg — thread-local self-message queue (same pattern as
   shard::slice), drained by drain_spsc_shared ahead of the SPSC
   consumers and woken via the shard's own drain Notify. PSYNC
   registration/unregistration and all replication fan-out on the owning
   shard's thread route through it.

2. Local (same-shard) writes now feed the replication plane at all:
   successful local writes push their wire bytes as ReplicateVerbatim
   BEFORE any await (mutation + record are one synchronous stretch —
   atomic w.r.t. the inline PSYNC snapshot capture). The drained message
   does backlog + offset + replica fan-out together, so the AOF leg no
   longer double-advances the offset (lsn = 0 when fan-out owns the
   advance; per-shard order is append order, same contract as the
   cross-shard legs).

3. ReplicationBacklog::new_at — the lazily-allocated backlog now seeds
   its byte positions at the CURRENT shard offset. An unseeded (0-based)
   backlog made every catch-up range read on a pre-written master fail
   as "evicted", feeding the reconnect loop even after registration.

4. replication::state::fanout_hint_active() — process-global AtomicBool
   (set on first replica attach, never cleared) gating all fan-out
   serialization with one Relaxed load, so non-replicating servers pay
   nothing on the hot path (S3.5a rationale).

Verified: master+replica manual soak shows zero reconnects / zero PSYNC
handler exits with KV + FT + graph live streaming; full lib suite,
replication_streaming/test/hardening, spsc_wake_floor green on monoio
and tokio feature sets.

author: Tin Dang
… + PSYNC snapshot backfill

Closes the last unsynchronized data plane (task #25: "synchronize KV
space and collections/indexes — vector, text, graph").

Live leg: graph mutations (GRAPH.* + Cypher writes) stream to replicas
as their DETERMINISTIC, id-pinned WAL records (GRAPH.ADDNODE <g> <id> …)
— the records gs.drain_wal() already produces for durability. Label and
property-key ids are a stateless FNV-1a hash (label_to_id), so master
and replica resolve identical ids with no interner-state sync. The
replica applies records through the same GraphReplayCollector restart
recovery uses (apply.rs intercept BEFORE generic dispatch — dispatch
parses USER syntax and would re-allocate ids). Replay's edge/SETPROP
resolution now also seeds node_map from write-buffer-resident nodes
(external_id ↔ NodeKey is the KeyData bijection), so one-record-at-a-
time streaming replay resolves endpoints applied by EARLIER records; a
no-op during restart recovery.

Snapshot leg: the FULLRESYNC RDB carries a moon-graph-store aux blob.
The master freezes every graph's write buffer to CSR segments (the
checkpoint's own "freeze is the only serialization path" contract) and
ships to_bytes() encodings + id-allocation cursors; the replica
installs them exactly like restart recovery — authoritative replace, id
floors restored (replication::graph_sync, format v1, bounds-checked).
An EMPTY blob (master has no graphs) drops replica-local graphs; an
ABSENT aux (pre-graph-sync master) warns and keeps. Mmap
(restart-loaded) segments export their mapped bytes verbatim
(CsrStorage::to_bytes Mmap arm was a silent empty Vec).

READONLY guard is now Cypher-aware: read-only GRAPH.QUERY
(MATCH/RETURN) is served by replicas; only write queries are rejected
(is_cypher_write_query token scan — false-positives route to rejection,
never false-negatives). Previously the blanket W flag rejected ALL
GRAPH.QUERY on replicas. Applied to monoio, sharded-tokio, and single
handlers.

New e2e tests/replication_graph.rs:
- replica_syncs_graph_live_stream — node/property/GRAPH.LIST parity,
  plus a zero-reconnect stream-health assertion (a reconnect/resync
  loop delivers the same data via repeated snapshots and would mask a
  dead live stream — the exact failure mode fixed in the previous
  commit).
- replica_backfills_graph_from_snapshot — pre-existing graph arrives
  via the snapshot, post-snapshot writes keep flowing live.

Unit tests: graph_sync round-trip (nodes/edges/props fidelity,
authoritative replace, empty-store drop, truncated-blob rejection).

author: Tin Dang
…iew P0/P1

Fixes every confirmed finding from the adversarial review of the
self-SPSC repair (8e6cb74) and graph-plane replication (dc603a3):

P0-1 — MULTI/EXEC bodies never replicated at --shards 1. EXEC persisted
its body through persist_txn_aof's AOF-only leg, the ONE local write
path that skipped the replication plane: the master committed durably
and the replica saw nothing (deterministic silent divergence). The txn
body now records each entry via record_local_write in the same
synchronous stretch as the (fully synchronous) transaction execution,
and persist_txn_aof takes a repl_recorded flag (lsn = 0, no offset
double-advance — same contract as the single-command legs). New e2e
replica_applies_multi_exec_bodies, red before / green after; the double
INCR doubles as a double-apply canary.

P0-2 — FULLRESYNC snapshot capture raced undrained local writes. The
prior design deferred backlog append + offset advance + live fan-out to
one queued event-loop message, so a mutation could be INSIDE the RDB
capture while still below the advertised snapshot offset — re-delivered
via backlog catch-up, double-applying non-idempotent commands.
Structural fix: record_local_write appends the backlog bytes and
advances the shard offset SYNCHRONOUSLY at write time (mutation and
replication ledger are one no-await stretch); only the live replica
try_send defers (new ShardMessage::ReplicaLiveFanout, replacing
ReplicateVerbatim). RegisterReplica now carries push_offset captured at
push time — an offset read at drain could cover a write whose fan-out
message is queued behind the registration (catch-up + live = twice).
Exactly-once now holds for every write/attach interleave; the
send_backlog_range/registration barrier protocol is unchanged.

P1-5 (+2 adjacent gaps found while fixing it) — the graph snapshot blob
lost all soft state living outside CSR segments: (a) the CSR byte
format has no validity section, so segment edge tombstones exist only
in the in-memory overlay (BOTH Heap and Mmap variants — the review
flagged Mmap, but heap to_bytes drops them identically); (b) freeze()
RETAINS cross-tier delta edges in the write buffer, so they are in NO
segment; (c) copy-up dead-shadow nodes tombstoning frozen rows never
freeze either. The master recovers all three from its WAL on restart;
a replica has no WAL — it resurrected deleted edges/nodes and silently
lost every cross-tier edge. Blob format v2 ships a per-segment
deleted-edge sidecar, the retained delta edges (original edge ids so
later streamed REMOVEEDGE records resolve; weight/props via the
existing encode_edge_record codec), and the dead-shadow list; install
re-applies all three (restore_dead_shadows now pub(crate)). Unit test
validity_overlay_delta_edges_and_dead_shadows_survive_export covers all
three sections plus the default-weight empty-record path.

P1-4 — streamed graph replay was O(N²): every replicated GRAPH.* record
re-scanned all write-buffer nodes AND all segment rows to pre-seed the
replay node_map (unbounded replication lag on bulk graph loads, backlog
eviction, resync storms). The map is gone: node existence resolves
lazily via the external_id <-> NodeKey bijection (O(1) write-buf hash
probe + per-segment MPH lookup), and the id-allocation floor is raised
per segment header max_node_id (new CsrStorage::max_node_id) instead of
per row. Restart recovery takes the same path — 541 graph lib tests +
38 freeze-boundary tests green.

P1-6 — FULLRESYNC graph export (inside the synchronous with_shard
block) no longer freezes untouched write buffers on every
reconnect-driven resync; a freeze that fails with live nodes present
now warns loudly instead of silently exporting a partial graph.

P2-7 — the shard self-queue drains unbounded per cycle: entries are
cheap try_sends/vec pushes, and a MAX_DRAIN_PER_CYCLE cut-off could
strand a replica's live bytes for a full tick.

P1-3 (drain-order inversion) is structurally resolved by the P0-2 fix:
backlog bytes are now appended at mutation time, in mutation order, so
execute_batch-vs-self-queue drain ordering no longer affects the
replication stream.

Verification: lib 4110 pass; e2e replication_streaming 4/4 (incl new
txn test), replication_graph 2/2, replication_test 5/5,
replication_hardening 5/5; graph lib 541 + graph_freeze_boundary 38;
fmt + clippy clean on default and tokio feature sets; tokio lib
replication tests 43 pass.

author: Tin Dang
…lay liveness, blob endpoint guard

Adversarial round-2 review of fa193ba confirmed every round-1 fix
correct under interleaving analysis, and surfaced four remaining gaps.
This commit addresses all of them:

Finding B (P0/P1) — TEMPORAL.INVALIDATE never replicated. The handler
drained the graph WAL but only fed the local WAL leg; a replica kept
valid_to = ∞ for entities the master invalidated. The drained
GraphTemporal record is a binary wal_v3 payload the RESP replication
link can't carry, and replaying the USER command would re-capture
wall_ms on the replica (valid_to divergence). The master now streams a
deterministic wall-clock-pinned internal form:

  TEMPORAL.INVALIDATE-AT <graph> <N|E> <entity_id> <wall_ms>

(serialize_invalidate_at / parse_invalidate_at in command/temporal.rs),
recorded via record_local_write in the same synchronous stretch as the
mutation — identical contract to the GRAPH.* leg. The replica applies
it through the same apply_invalidate the master ran, dropping the
locally-drained WAL payload (matching apply_graph's no-local-persist
model). New e2e REPL-GRAPH-03 (replica_applies_temporal_invalidate)
verifies temporal visibility converges via VALID_AT far-future queries
— confirmed RED with the master leg reverted, GREEN with it.

Finding F (P1) — streamed replay could resurrect a tombstoned node.
The P1-4 lazy-resolver rewrite made node_exists accept DEAD write-buf
entries (get_node does not filter deleted_lsn), so a stray SETPROP for
a node removed in an EARLIER streamed replay call re-registered it into
the live property index (set_node_property has no aliveness guard).
Split the predicate:
- node_present (AddNode dedup): ANY record of the id — live, dead
  tombstone/shadow, or CSR row. Slotmap ids are never reused, so an
  AddNode for a present id is only ever a full-history replay.
- node_alive (edge endpoints, SETPROP, SETLABEL, REMOVENODE): a
  write-buffer entry is authoritative (deleted_lsn == MAX ⇒ live);
  only absent ids fall back to CSR rows. Restores the old pre-seeded
  map's live-only iter_nodes() semantics and additionally closes the
  dead-shadow variant of the same hole.
New unit test test_streamed_replay_setprop_after_remove_does_not_
resurrect (two sequential replay_into calls) — RED before, GREEN after.

Finding E (P2) — graph snapshot install trusted blob delta edges.
add_edge_across_tiers_with_id's aliveness check only fires for
RESIDENT endpoints and the write buffer is empty at install time, so a
corrupted blob referencing a nonexistent node installed silently. The
install loop now verifies both endpoints against the just-installed
segments and drops the edge with tracing::warn! otherwise.

Finding A (P0, known limitation, NOT fixed here) — WS.*/MQ.* writes
never reach the replication plane. Full support needs deterministic
record forms (WS.CREATE mints a fresh UUIDv7 per execution — verbatim
streaming would diverge), replica apply arms, and snapshot coverage —
tracked as follow-up task #34 together with the pre-existing Lua-EVAL
(finding C) and expiry/eviction (finding D) gaps. Interim: a one-time
warn_unreplicated_plane fires when a WS/MQ mutation executes while a
replica is attached, so operators learn about the divergence at write
time instead of at failover.

Finding G (P2, note only): the P0-1 EXEC fix pushes one
ReplicaLiveFanout per body entry in a synchronous burst through the
unbounded self-queue drain — documented as a tail-latency vector at
the call site; each drain iteration is a try_send, so cheap unless
EXEC bodies grow unbounded.

Verification: lib 4111 green (both after fmt), replication_graph 3/3
(incl. new REPL-GRAPH-03), replication_streaming 4/4,
replication_hardening 5/5, replication_test (tokio) 5/5, tokio lib
replication 43/43, fmt clean, clippy clean on default and
runtime-tokio,jemalloc feature sets.

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 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replication now supports graph live streaming and snapshot installation, deterministic temporal invalidation, tombstone-safe replay, single-shard fanout ordering, MULTI/EXEC replication, Cypher-aware READONLY handling, and warnings for unsupported WS/MQ replication paths.

Changes

Replication and graph-plane changes

Layer / File(s) Summary
Graph replay and snapshot fidelity
src/command/temporal.rs, src/graph/..., src/persistence/redis_rdb.rs, src/replication/graph_sync.rs, src/replication/apply.rs
Graph replay resolves node presence and liveness lazily, preserves tombstones, serializes deterministic temporal invalidations, and exports or installs graph snapshots with CSR and sidecar state.
Single-shard registration and fanout
src/replication/backlog.rs, src/replication/master.rs, src/replication/state.rs, src/shard/...
Backlog offsets are seeded from current shard offsets, inline registration captures live-fanout boundaries, and a thread-local self-queue delivers live replication messages.
Command and transaction replication
src/server/conn/handler_monoio/..., src/server/conn/shared.rs, src/server/conn/handler_sharded/write.rs
Local writes, graph WAL records, temporal invalidations, and MULTI/EXEC bodies are recorded in the replication plane; WS/MQ unsupported mutations emit warnings.
Cypher-aware replica access and integration tests
src/server/conn/handler_*/..., tests/replication_graph.rs, tests/replication_streaming.rs
READONLY checks inspect GRAPH.QUERY queries, and ignored integration tests cover graph streaming, snapshot backfill, temporal invalidation, and transaction replication.
Replication release notes
CHANGELOG.md
The unreleased changelog documents the replication hardening and v0.7 graph-plane replication changes.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Master
  participant Replication
  participant Replica
  participant GraphStore
  Client->>Master: GRAPH.* or TEMPORAL.INVALIDATE
  Master->>GraphStore: apply mutation and produce WAL
  Master->>Replication: record backlog bytes and shard offset
  Replication->>Replica: stream live record
  Replica->>GraphStore: replay deterministic record
Loading

Possibly related PRs

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: v0.7 graph-plane replication with live stream and PSYNC snapshot backfill.
Description check ✅ Passed The summary is detailed and aligned with the template, though the checklist, performance impact, and notes sections are not filled in.
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/v0.7-graph-replication

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.

Actionable comments posted: 4

🤖 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.

Inline comments:
In `@CHANGELOG.md`:
- Around line 143-146: Clarify the CHANGELOG entry for
tests/replication_graph.rs as ignored/manual coverage, explicitly noting it is
not run by standard CI because the test is marked #[ignore]; alternatively, add
an explicit CI job invoking replication_graph with --ignored and document that
coverage.

In `@src/command/temporal.rs`:
- Around line 86-148: Add a unit-test module for serialize_invalidate_at and
parse_invalidate_at covering successful node and edge round trips, including a
negative wall_ms, and asserting malformed argument lists or fields return None.
Use the existing Frame and Bytes types to construct parser inputs and verify all
decoded values match the originals.

In `@src/replication/master.rs`:
- Around line 750-758: Ensure replica registration is cleaned up when catch-up
fails between push_register_replica_inline and drain_replica_inline_single_shard
in both FullResync and PartialResync. Handle send_backlog_range errors by
enqueueing or otherwise performing UnregisterReplica before propagating the
error, while preserving existing cleanup for later drain failures.

In `@tests/replication_graph.rs`:
- Around line 134-135: The replication graph test uses ports shared with
replication_streaming, causing parallel test conflicts. Update the master_addr
and replica_addr values in the replication graph test to a unique,
non-overlapping port pair.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b9135d5f-b5c8-42c9-9087-639c8f240890

📥 Commits

Reviewing files that changed from the base of the PR and between fb1565b and 2ba72c7.

📒 Files selected for processing (31)
  • CHANGELOG.md
  • src/command/temporal.rs
  • src/graph/csr/mmap.rs
  • src/graph/csr/storage.rs
  • src/graph/replay.rs
  • src/graph/store.rs
  • src/persistence/redis_rdb.rs
  • src/replication/apply.rs
  • src/replication/backlog.rs
  • src/replication/graph_sync.rs
  • src/replication/master.rs
  • src/replication/mod.rs
  • src/replication/state.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/handler_monoio/ft.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_monoio/txn.rs
  • src/server/conn/handler_monoio/write.rs
  • src/server/conn/handler_sharded/dispatch.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/server/conn/handler_sharded/write.rs
  • src/server/conn/handler_single.rs
  • src/server/conn/shared.rs
  • src/shard/conn_accept.rs
  • src/shard/dispatch.rs
  • src/shard/event_loop.rs
  • src/shard/mod.rs
  • src/shard/self_msg.rs
  • src/shard/spsc_handler.rs
  • tests/replication_graph.rs
  • tests/replication_streaming.rs
💤 Files with no reviewable changes (1)
  • src/shard/conn_accept.rs

Comment thread CHANGELOG.md
Comment on lines +143 to +146
- New e2e `tests/replication_graph.rs`: live-stream parity (nodes, properties,
GRAPH.LIST) with a zero-reconnect stream-health assertion that would have
caught the masked dead stream, plus snapshot backfill + post-snapshot live
growth.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify this as ignored/manual coverage. tests/replication_graph.rs is #[ignore], and CI only runs replication_hardening, so the zero-reconnect assertion won’t run in standard test jobs. Either note that here or add an explicit --ignored job for replication_graph.

🤖 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 `@CHANGELOG.md` around lines 143 - 146, Clarify the CHANGELOG entry for
tests/replication_graph.rs as ignored/manual coverage, explicitly noting it is
not run by standard CI because the test is marked #[ignore]; alternatively, add
an explicit CI job invoking replication_graph with --ignored and document that
coverage.

Comment thread src/command/temporal.rs
Comment on lines +86 to +148
/// Serialize the deterministic, wall-clock-pinned replication form of
/// TEMPORAL.INVALIDATE (v0.7 graph replication, adversarial round-2 finding
/// B): `TEMPORAL.INVALIDATE-AT <graph> <N|E> <entity_id> <wall_ms>`.
///
/// The user command captures `wall_ms` at execution time, so streaming it
/// verbatim would let master and replica disagree on `valid_to`; and the
/// drained `GraphTemporal` WAL record is a binary wal_v3 payload the RESP
/// replication link cannot carry. This internal RESP form pins the master's
/// wall clock; the replica applies it via `apply_invalidate` with the SAME
/// `wall_ms` (see `replication::apply`).
#[cfg(feature = "graph")]
pub fn serialize_invalidate_at(
graph_name: &[u8],
is_node: bool,
entity_id: u64,
wall_ms: i64,
) -> Vec<u8> {
fn write_bulk(buf: &mut Vec<u8>, data: &[u8]) {
let mut n = itoa::Buffer::new();
buf.push(b'$');
buf.extend_from_slice(n.format(data.len()).as_bytes());
buf.extend_from_slice(b"\r\n");
buf.extend_from_slice(data);
buf.extend_from_slice(b"\r\n");
}
let mut id_buf = itoa::Buffer::new();
let mut ms_buf = itoa::Buffer::new();
let mut buf = Vec::with_capacity(96 + graph_name.len());
buf.extend_from_slice(b"*5\r\n");
write_bulk(&mut buf, b"TEMPORAL.INVALIDATE-AT");
write_bulk(&mut buf, graph_name);
write_bulk(&mut buf, if is_node { b"N" } else { b"E" });
write_bulk(&mut buf, id_buf.format(entity_id).as_bytes());
write_bulk(&mut buf, ms_buf.format(wall_ms).as_bytes());
buf
}

/// Parse the argument list of a replicated `TEMPORAL.INVALIDATE-AT` record
/// (inverse of [`serialize_invalidate_at`], minus the command name).
/// Returns `(graph_name, is_node, entity_id, wall_ms)` or `None` on any
/// malformed field — the replica warns and skips rather than diverging
/// silently on garbage.
#[cfg(feature = "graph")]
pub fn parse_invalidate_at(args: &[Frame]) -> Option<(Bytes, bool, u64, i64)> {
if args.len() != 4 {
return None;
}
let bulk = |f: &Frame| -> Option<Bytes> {
match f {
Frame::BulkString(b) | Frame::SimpleString(b) => Some(b.clone()),
_ => None,
}
};
let graph_name = bulk(&args[0])?;
let is_node = match bulk(&args[1])?.as_ref() {
b"N" => true,
b"E" => false,
_ => return None,
};
let entity_id: u64 = std::str::from_utf8(&bulk(&args[2])?).ok()?.parse().ok()?;
let wall_ms: i64 = std::str::from_utf8(&bulk(&args[3])?).ok()?.parse().ok()?;
Some((graph_name, is_node, entity_id, wall_ms))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for existing unit tests exercising the invalidate-at codec.
rg -nP 'serialize_invalidate_at|parse_invalidate_at' -g '*.rs' -C2

Repository: pilotspace/moon

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## temporal.rs outline\n'
ast-grep outline src/command/temporal.rs --view expanded || true

printf '\n## relevant code around serialize/parse\n'
sed -n '1,220p' src/command/temporal.rs | cat -n

printf '\n## search for invalidate-at and related tests\n'
rg -n --hidden -S 'TEMPORAL\.INVALIDATE-AT|serialize_invalidate_at|parse_invalidate_at|TEMPORAL\.INVALIDATE|consistency-test|consistency test|unit test' src test tests . || true

printf '\n## command module files\n'
git ls-files 'src/command/**' | sed -n '1,200p'

Repository: pilotspace/moon

Length of output: 50371


Add a round-trip unit test for TEMPORAL.INVALIDATE-AT (src/command/temporal.rs:97-148)
serialize_invalidate_at/parse_invalidate_at should have a direct round-trip test covering node/edge cases, negative wall_ms, and malformed-input rejection. The end-to-end replication tests don’t exercise this codec in isolation.

🤖 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/command/temporal.rs` around lines 86 - 148, Add a unit-test module for
serialize_invalidate_at and parse_invalidate_at covering successful node and
edge round trips, including a negative wall_ms, and asserting malformed argument
lists or fields return None. Use the existing Frame and Bytes types to construct
parser inputs and verify all decoded values match the originals.

Source: Coding guidelines

Comment thread src/replication/master.rs
Comment on lines +750 to +758
let reg = push_register_replica_inline(&repl_state)?;
let reg_offset = reg
.reg_rx
.recv_async()
.await
.map_err(|_| anyhow::anyhow!("event loop dropped registration reply"))?;
send_backlog_range(&mut stream, &backlog_slot, snapshot_offset, reg_offset).await?;

drain_replica_inline_single_shard(reg, replica_addr, stream, repl_state, dispatch_tx)
.await?;
drain_replica_inline_single_shard(reg, replica_addr, stream, repl_state).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect wal_append_and_fanout and any replica_txs pruning on failed send.
rg -nP -C4 'fn wal_append_and_fanout' src/shard/
rg -nP -C3 'replica_txs\.(retain|remove|swap_remove)' src/

Repository: pilotspace/moon

Length of output: 1421


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/shard/spsc_handler.rs around UnregisterReplica / ReplicaLiveFanout / wal_append_and_fanout ---'
sed -n '2538,2595p' src/shard/spsc_handler.rs
echo
sed -n '3378,3455p' src/shard/spsc_handler.rs

echo '--- src/replication/master.rs around the cited resync paths ---'
sed -n '730,790p' src/replication/master.rs

Repository: pilotspace/moon

Length of output: 9673


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- registration helper / drain helper in src/replication/master.rs ---'
rg -n -C5 'push_register_replica_inline|drain_replica_inline_single_shard|UnregisterReplica|reg_rx|replica_txs' src/replication/master.rs

echo
echo '--- exact definitions around the helper(s) ---'
sed -n '1,260p' src/replication/master.rs

Repository: pilotspace/moon

Length of output: 15282


Replica registration needs cleanup on catch-up failures. Both FullResync and PartialResync register the replica before send_backlog_range(), so an error returns before drain_replica_inline_single_shard() can enqueue UnregisterReplica. Since replica_txs only prunes on that message and fanout doesn’t drop dead senders, the stale entry can stick around for the shard lifetime.

🤖 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/replication/master.rs` around lines 750 - 758, Ensure replica
registration is cleaned up when catch-up fails between
push_register_replica_inline and drain_replica_inline_single_shard in both
FullResync and PartialResync. Handle send_backlog_range errors by enqueueing or
otherwise performing UnregisterReplica before propagating the error, while
preserving existing cleanup for later drain failures.

Comment on lines +134 to +135
let master_addr = "127.0.0.1:16730";
let replica_addr = "127.0.0.1:16731";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- replication_graph.rs around cited lines ---\n'
sed -n '120,160p' tests/replication_graph.rs

printf '\n--- replication_streaming.rs port literals ---\n'
rg -n '16730|16731|master_addr|replica_addr' tests/replication_streaming.rs tests/replication_graph.rs

printf '\n--- all occurrences in tests of the two ports ---\n'
rg -n '127\.0\.0\.1:16730|127\.0\.0\.1:16731|16730|16731' tests

printf '\n--- integration test files list ---\n'
git ls-files 'tests/*.rs'

Repository: pilotspace/moon

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- replication_graph.rs around cited lines ---'
sed -n '120,160p' tests/replication_graph.rs

echo
echo '--- replication_streaming.rs port literals ---'
rg -n '16730|16731|master_addr|replica_addr' tests/replication_streaming.rs tests/replication_graph.rs

echo
echo '--- all occurrences in tests of the two ports ---'
rg -n '127\.0\.0\.1:16730|127\.0\.0\.1:16731|16730|16731' tests

echo
echo '--- integration test files list ---'
git ls-files 'tests/*.rs'

Repository: pilotspace/moon

Length of output: 17491


Use a unique port pair here. tests/replication_streaming.rs also binds 127.0.0.1:16730/16731, so these ignored integration binaries can contend for the same ports when cargo runs them in parallel. Pick a non-overlapping pair for this test.

🤖 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 `@tests/replication_graph.rs` around lines 134 - 135, The replication graph
test uses ports shared with replication_streaming, causing parallel test
conflicts. Update the master_addr and replica_addr values in the replication
graph test to a unique, non-overlapping port pair.

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