Skip to content

feat(wal): typed wal_append channel end-to-end + WS.CREATE created_at (Wave B stage 1 / kernel M1)#289

Merged
pilotspacex-byte merged 3 commits into
mainfrom
feat/v0.7-plane-repl-wave-b
Jul 12, 2026
Merged

feat(wal): typed wal_append channel end-to-end + WS.CREATE created_at (Wave B stage 1 / kernel M1)#289
pilotspacex-byte merged 3 commits into
mainfrom
feat/v0.7-plane-repl-wave-b

Conversation

@pilotspacex-byte

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

Copy link
Copy Markdown
Contributor

Summary

Storage-kernel M1 stage 1 / Wave B foundation: the cross-thread WAL append channel now preserves the caller's WalRecordType end-to-end — retiring the design where every typed record (XactCommit, WorkspaceCreate/Drop, MqCreate/Ack, GraphTemporal) was pre-framed by its producer, shipped as raw bytes, and blanket re-wrapped as an outer Command record by the event-loop drain. That nesting is what made XactCommit replay functionally dead, mislabeled a txn-id as a WAL LSN, and forced every replayer to grow bespoke unwrap arms (PR #286's fix class).

K1a — typed channel

  • Channel payload: Bytes(WalRecordType, Bytes). All 15 production send sites across both runtimes converted (compiler-enforced via the arity change); producers' write_wal_v3_record pre-framing deleted — single framing, real LSN assignment at wal.append.
  • TEMPORAL.INVALIDATE producer framing (added in fix(shard): TEMPORAL/MQ WAL v3 replay data loss on kill-9 (task #42) #286) replaced by the typed channel; the GraphStore temporal_wal_pending side-channel carries the payload from dispatch to the SPSC drain (single producer, single .take() consumer on the same shard thread).
  • No format bump; mixed-segment compatibility guaranteed: all nested-Command unwrap arms and the GraphTemporal legacy-raw fallback remain — old segments replay identically; new records hit the direct-type arms.
  • XactCommit recovery decision, documented in recovery.rs: Phase 4 counts-but-never-dispatches (the txn's inner ops already ride --wal-kv-log Command records or the AOF authority — applying would risk double-apply); the last-resort Phase-4b fallback (replay_wal_v3_dir_commands) DOES apply it — it runs exactly when neither coverage source exists, so the preconditions are complementary (asymmetry note added after review).

K1b — WS.CREATE created_at durability

encode/decode_workspace_create gains a versioned created_at_ms suffix (old-length records decode as before, created_at 0); restart now restores the real creation time instead of zeroing it — prerequisite for Wave B's WS.CREATE.APPLY replication record.

Verification

  • RED-first unit tests: on-disk outer type is WorkspaceCreate (not Command) through the exact channel+drain pattern; created_at round-trip + legacy-format tolerance + negative-value handling.
  • macOS gates: lib 4148/4148, crash_recovery_temporal_mq 1/1, shardslice_live 6/6 (incl. ssm3 WS restart), crash_recovery_wal_recycle_legacy 1/1, graph_durability g4/g5, clippy -D warnings default + tokio, fmt.
  • Linux VM (fresh ELF build, MOON_BIN pinned): same crash suites green, lib 4168/4168.
  • Adversarial review (independent agent): verdict SHIP. Traced all 15 producer sites, verified the 3-generation replay matrix has mutually-exclusive arms (no double-apply), the temporal side-channel has exactly one producer/consumer on one thread, LSN semantics unchanged (the old inner-frame lsn was never read), and 3 feature-matrix builds compile clean. Its one P2 (XactCommit Phase-4/Phase-4b asymmetry undocumented) is addressed in the follow-up commit.
  • Bench waiver: same channel, same drain site, one enum tag added to the payload tuple; no hot-path KV dispatch changes.

Notes

  • Pre-existing (out of scope, recorded): handler_sharded TEMPORAL.INVALIDATE lacks the replication-fanout block its monoio twin has (tokio has no master-side replication machinery).
  • Next Wave B stages stack on this: MQ effect records (dual-plane), replica apply arms, FULLRESYNC snapshot legs (task GPU: CI/CD infrastructure for CUDA builds and testing #34).

Summary by CodeRabbit

  • Bug Fixes
    • Improved WAL persistence and recovery to preserve the correct record type across restarts.
    • Workspace created_at is now stored and replayed correctly; legacy records continue to load with the prior default.
    • Transaction commit and graph/temporal invalidation records are handled more reliably during recovery and replication.
  • Documentation
    • Updated release documentation with the new backward-compatible WAL persistence and replay behavior.

…(K1a+K1b)

Stage 1 of Wave B / storage-kernel M1 (storage-audit-2026-07-12-wal.md
enhancement #2 + wave-b-ws-mq-scope-2026-07-12.md item 1). Two fixes, shipped
together because they compile-depend on each other in three shared producer
files (handler_monoio/write.rs, handler_sharded/write.rs, uring_handler.rs):
K1b's `encode_workspace_create` signature change lands in the same hunks as
K1a's unframing, so splitting them into strictly sequential commits would
require a throwaway intermediate revert with no compile-time value.

K1a — preserve the caller's WalRecordType through the wal_append channel
--------------------------------------------------------------------------
`ShardDatabases::wal_append`/`try_wal_append_required` and
`mq_exec::wal_append_on_slice` took a plain `Bytes` payload; the shard
event-loop's 1ms-tick drain (`event_loop.rs`, two sites) unconditionally
re-wrapped every message as an outer `WalRecordType::Command` record. Every
non-Command producer (XactCommit, WorkspaceCreate/Drop, MqCreate/Ack,
GraphTemporal) worked around this by pre-framing its own record with
`write_wal_v3_record` and sending the ALREADY-FRAMED bytes through — a
second, nested WAL frame inside the outer Command frame, recovered on
replay via a bespoke per-record-type unwrap. The XactCommit pre-framing
additionally passed `txn_id` into the inner frame's `lsn` field, mislabeling
a transaction id as a WAL LSN.

Fixed structurally: the channel item is now `(WalRecordType, Bytes)` — the
real type plus the UNFRAMED payload — threaded through
`ShardSlice::wal_append_tx` / `ShardDatabases::wal_append_txs` end-to-end.
The drain calls `wal.append(record_type, &payload)` directly, so the WAL
writer assigns the real LSN and does the single framing. Every producer's
pre-framing was deleted: handler_monoio/handler_sharded write.rs (WS.CREATE/
DROP) and txn.rs (XactCommit — the txn_id-as-lsn mislabel is gone with it),
shard/uring_handler.rs's WS batch path, shard/mq_exec.rs (MqCreate/MqAck),
and command::temporal::apply_invalidate (GraphTemporal — PR #286 had just
added this record's pre-framing; deleted in favor of the typed channel).
`apply_invalidate` now RETURNS the raw payload instead of pushing it into
`GraphStore::wal_pending` (which stays exclusively Command-typed RESP
bytes). The one caller that cannot change its Frame-only return signature
without a ~90-call-site test ripple (`command::graph::dispatch_graph_command`,
the cross-shard GraphCommand entry point) stashes the payload in a new
`GraphStore::temporal_wal_pending` side-channel instead, which its real
caller (`shard/spsc_handler.rs`'s GraphCommand arm) `.take()`s right after
dispatch.

Replay is fully backward compatible, no format bump: every existing
direct-type match arm (replay_workspace_wal, replay_mq_wal,
replay_temporal_wal in shared_databases.rs) already had a nested-Command
unwrap fallback plus, for GraphTemporal, a legacy-raw fallback for even
older un-nested records — both untouched, so segments written before this
fix keep replaying exactly as before.

persistence::recovery.rs Phase 4's on_command closure had no XactCommit arm
at all (silent `_ => {}`) — now reachable for the first time because the
outer type used to always be Command. Decision: an EXPLICIT documented
no-op (counts toward commands_replayed, never dispatches). The
forward-image KV payload is redundant in every reachable config — the
transaction's individual SET/DEL ops already ride either this same Phase-4
WAL replay as ordinary Command records (--wal-kv-log on) or the AOF, the KV
recovery authority in every config (Phase 4b falls back to it whenever
kv_commands_replayed == 0). Decoding it would be a no-op at best and risks
double-applying a non-idempotent op at worst. wal_v3::replay's
replay_wal_v3_dir_commands (the legacy last-resort-fallback path) already
had a correct replay_xact_commit call for the real outer type — untouched,
and now reachable for the first time too.

K1b — WS.CREATE's created_at survives restart
--------------------------------------------------------------------------
`encode_workspace_create`/`decode_workspace_create` only ever serialized
`[ws_id][name_len][name]` — the created_at computed at WS.CREATE time never
reached the WAL payload, so replay_workspace_wal hardcoded `created_at: 0`
on every restart. Fixed with a versioned, backward-compatible layout: new
records append a trailing `created_at_ms: i64 LE`; the decoder accepts both
the old (20+name_len-byte) and new (20+name_len+8-byte) lengths, returning
created_at = 0 for old records (matching prior restart behavior exactly —
no format bump, mixed-segment compatible). All three producers now pass the
already-computed created_at through; replay threads the real value into
WorkspaceMetadata.

Tests
--------------------------------------------------------------------------
- src/persistence/wal_v3/segment.rs:
  test_typed_channel_preserves_workspace_create_outer_type — sends a
  WS.CREATE record through the exact (WalRecordType, Bytes) channel + drain
  pattern event_loop.rs uses, asserts the on-disk outer type is
  WorkspaceCreate (not Command) and the payload decodes directly.
- src/workspace/wal.rs: updated roundtrip tests for the 3-arg signature;
  added test_workspace_create_legacy_format_no_created_at (pre-K1b
  20+name_len-byte payload still decodes, created_at=0) and
  test_workspace_create_created_at_negative_value.
- tests/quickwins_red.rs: updated qw2 for the new wal_append/
  try_wal_append_required/set_wal_append_tx signatures (PIN test, no
  behavior change).

Gates (all green unless noted)
--------------------------------------------------------------------------
- cargo check/test --release --lib (default features): 4148 passed, 0 failed
- cargo check/clippy --no-default-features --features runtime-tokio,jemalloc:
  clean (-D warnings)
- cargo fmt --check: clean
- crash_recovery_temporal_mq -- --ignored: 1/1 pass
- shardslice_live (incl. ssm3 WS registry restart): 6/6 pass
- crash_recovery_wal_recycle_legacy -- --ignored: 1/1 pass
- crash_recovery_graph_durability g4/g5 -- --ignored: 2/2 pass
- Full tokio-matrix integration suite (--no-fail-fast): all green except 5
  pre-existing/environmental failures with ZERO overlap with this diff
  (confirmed via `git diff --stat`): cmd_flush_dbsize_debug_memory uses a
  removed --persistence-dir CLI flag (stale since a --dir rename, unrelated
  file untouched here); mem_watchdog/oom_bypass_closure/spsc_two_db fail on
  "MOONERR diskfull" — this checkout's volume is at 95% disk usage, tripping
  the <5% free-space write-pause guard (a known environment gotcha, not a
  code defect); memory_prometheus_kinds expects 7 Prometheus memory kinds
  but the binary now reports 8 — pre-existing drift in an unrelated metrics
  file this change never touches.

author: Tin Dang
…ly asymmetry

Adversarial review P2 on the K1a typed-channel change: recovery.rs
Phase 4 treats XactCommit as a documented no-op while the legacy
fallback replay_wal_v3_dir_commands applies it — both newly reachable
for real-typed records. Documents why both are correct: the fallback
runs only when Phase 4 replayed zero KV commands AND no AOF exists,
i.e. exactly when the redundancy argument for the no-op does not hold.

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
📝 Walkthrough

Walkthrough

The PR preserves producer WAL record types through typed append channels, removes producer-side framing, adds direct GraphTemporal persistence, persists WS.CREATE timestamps with backward-compatible decoding, and updates XactCommit replay accounting.

Changes

WAL persistence and recovery

Layer / File(s) Summary
Typed WAL append pipeline
src/shard/..., src/server/..., src/transaction/abort.rs, tests/quickwins_red.rs
WAL channels now carry (WalRecordType, Bytes) pairs, and transaction, MQ, graph, rollback, replication, SWAPDB, and workspace producers append typed unframed payloads.
Temporal invalidation WAL flow
src/command/..., src/graph/store.rs, src/server/..., src/replication/apply.rs, src/shard/spsc_handler.rs
apply_invalidate returns a GraphTemporal payload, which is buffered separately and emitted once without replica-side WAL draining.
Workspace creation timestamp persistence
src/workspace/wal.rs, src/shard/uring_handler.rs, src/server/conn/handler_*/write.rs, src/shard/shared_databases.rs
WS.CREATE payloads include created_at_ms; decoding accepts legacy payloads and replay restores the decoded timestamp.
Replay accounting and compatibility documentation
src/persistence/recovery.rs, CHANGELOG.md
XactCommit records are counted during Phase 4 replay without command dispatch, and WAL compatibility behavior is documented.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GraphCommand
  participant GraphStore
  participant ShardDatabases
  participant WALWriter
  Client->>GraphCommand: TEMPORAL.INVALIDATE
  GraphCommand->>GraphStore: apply_invalidate()
  GraphStore-->>GraphCommand: GraphTemporal payload
  GraphCommand->>ShardDatabases: wal_append(GraphTemporal, payload)
  ShardDatabases->>WALWriter: typed payload
  WALWriter-->>ShardDatabases: persisted WAL record
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: typed wal_append propagation and WS.CREATE created_at persistence.
Description check ✅ Passed The description is detailed and relevant, but it omits the checklist section and doesn't clearly separate performance impact.
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-plane-repl-wave-b

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/graph/store.rs (1)

249-266: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Mark GraphStore dirty for temporal invalidations. persist_graph_at_checkpoint() (src/graph/recovery.rs) returns early when GraphStore::is_dirty() is false, so a lone TEMPORAL.INVALIDATE can leave the graph unsnapshotted while its GraphTemporal WAL record becomes eligible for recycle. Set gs.mark_dirty() when staging temporal_wal_pending.

🤖 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/graph/store.rs` around lines 249 - 266, Mark the GraphStore dirty when
staging a temporal WAL payload so temporal-only invalidations trigger checkpoint
persistence. Update the TEMPORAL.INVALIDATE flow that assigns
GraphStore::temporal_wal_pending, calling GraphStore::mark_dirty() in the same
path before or alongside the assignment; leave normal WAL handling unchanged.
🤖 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 `@src/workspace/wal.rs`:
- Around line 28-33: Update the documentation for decode_workspace_create to
accurately describe its lenient length handling: retain the compatibility
behavior for the legacy base length, explain that shorter-than-timestamp or
longer payloads are accepted according to the current implementation, and remove
the claim that all other lengths are malformed.

---

Outside diff comments:
In `@src/graph/store.rs`:
- Around line 249-266: Mark the GraphStore dirty when staging a temporal WAL
payload so temporal-only invalidations trigger checkpoint persistence. Update
the TEMPORAL.INVALIDATE flow that assigns GraphStore::temporal_wal_pending,
calling GraphStore::mark_dirty() in the same path before or alongside the
assignment; leave normal WAL handling unchanged.
🪄 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: b5d68afd-ef5f-47a6-856f-b461d427a6b2

📥 Commits

Reviewing files that changed from the base of the PR and between 7747998 and c1f2330.

📒 Files selected for processing (21)
  • CHANGELOG.md
  • src/command/graph/mod.rs
  • src/command/temporal.rs
  • src/graph/store.rs
  • src/persistence/recovery.rs
  • src/persistence/wal_v3/segment.rs
  • src/replication/apply.rs
  • src/server/conn/handler_monoio/txn.rs
  • src/server/conn/handler_monoio/write.rs
  • src/server/conn/handler_sharded/txn.rs
  • src/server/conn/handler_sharded/write.rs
  • src/shard/coordinator.rs
  • src/shard/event_loop.rs
  • src/shard/mq_exec.rs
  • src/shard/shared_databases.rs
  • src/shard/slice.rs
  • src/shard/spsc_handler.rs
  • src/shard/uring_handler.rs
  • src/transaction/abort.rs
  • src/workspace/wal.rs
  • tests/quickwins_red.rs

Comment thread src/workspace/wal.rs Outdated
…h handling

CodeRabbit (PR #289): the doc claimed "any other length is malformed",
but the decoder deliberately tolerates 0-7 trailing bytes (created_at 0)
and ignores extra bytes past the timestamp — the mixed-segment compat +
future-suffix contract. Reword the doc to describe the actual behavior;
no code change.

author: Tin Dang

@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/workspace/wal.rs (1)

28-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test every documented suffix-length compatibility case.

The legacy test covers zero trailing bytes, but the contract also promises that 1–7 bytes default to zero and bytes after a complete timestamp are ignored. Add table-driven cases to prevent replay compatibility regressions.

Also applies to: 135-153

🤖 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/workspace/wal.rs` around lines 28 - 34, Add table-driven coverage for the
WAL payload decoder’s documented suffix-length behavior, including 1–7 trailing
bytes producing created_at_ms = 0 and payloads with more than 8 trailing bytes
ignoring bytes after the complete timestamp. Extend the existing legacy test
near the decoder to cover zero bytes and all intermediate lengths while
preserving the current expected decoded workspace ID and name.
🤖 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/workspace/wal.rs`:
- Around line 28-34: Add table-driven coverage for the WAL payload decoder’s
documented suffix-length behavior, including 1–7 trailing bytes producing
created_at_ms = 0 and payloads with more than 8 trailing bytes ignoring bytes
after the complete timestamp. Extend the existing legacy test near the decoder
to cover zero bytes and all intermediate lengths while preserving the current
expected decoded workspace ID and name.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 061f0dbe-788b-457c-8bce-f31b88a95f9e

📥 Commits

Reviewing files that changed from the base of the PR and between c1f2330 and b68c3f1.

📒 Files selected for processing (1)
  • src/workspace/wal.rs

@pilotspacex-byte
pilotspacex-byte merged commit 7a9004a into main Jul 12, 2026
15 checks passed
@TinDang97
TinDang97 deleted the feat/v0.7-plane-repl-wave-b branch July 12, 2026 02:11
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