Skip to content

State WAL replacement#3701

Open
cody-littley wants to merge 42 commits into
mainfrom
cjl/flatkv-wal
Open

State WAL replacement#3701
cody-littley wants to merge 42 commits into
mainfrom
cjl/flatkv-wal

Conversation

@cody-littley

Copy link
Copy Markdown
Contributor

Describe your changes and provide context

This WAL is intended to replace all existing WALs in both SS and SC. Instead of having multiple WALs for multiple different services, we can maintain just a single WAL which is used universally across all parts of the storage stack.

This PR does not integrate the changes, it just creates a WAL utility. Integration work will happen in future PRs. We've got a pre-existing WAL, but that implementation has some serious issues. Namely, the WAL library we currently use has very inefficient garbage collection (it rewrites garbage collected files, as opposed to dropping files as a whole when data in file is all stale).

Testing performed to validate your change

unit tests

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJul 15, 2026, 7:58 PM

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.16966% with 292 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.06%. Comparing base (c6ab0be) to head (ed97f93).
⚠️ Report is 22 commits behind head on main.

Files with missing lines Patch % Lines
sei-db/seiwal/seiwal_file.go 64.68% 52 Missing and 43 partials ⚠️
sei-db/seiwal/seiwal_impl.go 76.24% 58 Missing and 33 partials ⚠️
sei-db/seiwal/seiwal_serializing.go 85.63% 22 Missing and 5 partials ⚠️
sei-db/state_db/statewal/state_wal_impl.go 81.30% 12 Missing and 11 partials ⚠️
sei-db/seiwal/seiwal_offline.go 71.83% 10 Missing and 10 partials ⚠️
sei-db/seiwal/seiwal_iterator.go 88.18% 9 Missing and 6 partials ⚠️
sei-db/seiwal/seiwal_lock.go 50.00% 5 Missing and 4 partials ⚠️
...ei-db/state_db/statewal/state_wal_serialization.go 79.48% 4 Missing and 4 partials ⚠️
sei-db/seiwal/metrics.go 77.77% 1 Missing and 1 partial ⚠️
sei-db/seiwal/seiwal.go 71.42% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3701      +/-   ##
==========================================
- Coverage   59.85%   59.06%   -0.79%     
==========================================
  Files        2278     2203      -75     
  Lines      189140   180599    -8541     
==========================================
- Hits       113202   106665    -6537     
+ Misses      65861    64541    -1320     
+ Partials    10077     9393     -684     
Flag Coverage Δ
sei-chain-pr 76.12% <76.12%> (?)
sei-db 70.41% <ø> (ø)
sei-db-state-db ?
sei-db-state-db-pr 83.33% <83.33%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sei-db/seiwal/seiwal_config.go 100.00% <100.00%> (ø)
sei-db/state_db/statewal/state_wal_config.go 100.00% <100.00%> (ø)
sei-db/seiwal/metrics.go 77.77% <77.77%> (ø)
sei-db/seiwal/seiwal.go 71.42% <71.42%> (ø)
...ei-db/state_db/statewal/state_wal_serialization.go 79.48% <79.48%> (ø)
sei-db/seiwal/seiwal_lock.go 50.00% <50.00%> (ø)
sei-db/seiwal/seiwal_iterator.go 88.18% <88.18%> (ø)
sei-db/seiwal/seiwal_offline.go 71.83% <71.83%> (ø)
sei-db/state_db/statewal/state_wal_impl.go 81.30% <81.30%> (ø)
sei-db/seiwal/seiwal_serializing.go 85.63% <85.63%> (ø)
... and 2 more

... and 87 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cody-littley cody-littley marked this pull request as ready for review July 6, 2026 17:35
@cody-littley cody-littley requested a review from blindchaser July 6, 2026 17:35
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
New durability-critical storage code with complex crash/rollback semantics; risk is moderated because this PR only lands the library and tests, not production integration.

Overview
Introduces a new seiwal write-ahead log library meant to replace fragmented WAL usage across the storage stack (integration deferred to follow-up PRs). It provides a generic, monotonic-index WAL[T] with async append/flush, file rotation and whole-file pruning (instead of rewriting GC’d segments), crash recovery (orphan sealing, torn-tail tolerance, rollback remnants), and iterators that read hard-linked point-in-time snapshots so pruning does not block replay.

The on-disk format uses sealed/mutable files, CRC-framed records, a directory wal.lock, and offline helpers GetRange, PruneAfter, and VerifyIntegrity. NewGenericWAL adds background serialization with OpenTelemetry metrics (seiwal_* counters/gauges).

Adds statewal, a block-scoped StateWAL over []*proto.NamedChangeSet (ordered Write / SignalEndOfBlock, flush only completed blocks) built on seiwal. .gitignore now ignores sei-db/seiwal/walsim/bin/.

Reviewed by Cursor Bugbot for commit ed97f93. Bugbot is set up for automated code reviews on this repo. Configure here.

seidroid[bot]

This comment was marked as low quality.

Comment thread sei-db/state_db/statewal/state_wal_impl.go
Comment thread sei-db/state_db/statewal/state_wal_entry.go Outdated
seidroid[bot]

This comment was marked as low quality.

Comment thread sei-db/state_db/statewal/state_wal_impl.go Outdated
if err := w.enforceWriteOrdering(blockNumber); err != nil {
return fmt.Errorf("write rejected: %w", err)
}
if err := w.sendToSerializer(dataToBeSerialized{entry: NewEntry(blockNumber, cs)}); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Write keeps caller-owned changesets and serializes them later. Because Write returns after enqueueing, callers can mutate or reuse cs before serializerLoop runs, causing the WAL to persist changed data or race with the caller

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This pattern is intentional for performance, but not well documented.

I updated all godocs to say that it is unsafe to modify slices passed into methods or slices received from methods.

seidroid[bot]

This comment was marked as low quality.

Comment thread sei-db/seiwal/seiwal_serializing.go
Comment thread sei-db/seiwal/seiwal_file.go
Comment thread sei-db/seiwal/seiwal_file.go
Comment thread sei-db/seiwal/seiwal_iterator.go
Comment thread sei-db/state_db/statewal/state_wal.go Outdated
seidroid[bot]

This comment was marked as low quality.

claude[bot]

This comment was marked as low quality.

seidroid[bot]

This comment was marked as low quality.

index: index,
serialize: func() ([]byte, error) { return s.serialize(data) },
}
if err := s.submit(req); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1. two implementation notes if we add the gate here: (1) the gate needs to be seeded from inner.Bounds() at construction (same as newWAL does for lastAppendIndex), otherwise the first append after a reopen goes unchecked; (2) TestGenericWALAppendOrdering currently asserts the async-brick behavior as expected, so its assertions need to flip to a synchronous rejection.

// from an interrupted rollback and seals any unsealed file left behind by a prior session. After it returns,
// every record lives in a sealed file whose name matches its content, with no orphans remaining. Shared by
// the WAL constructor and the offline GetRange/PruneAfter utilities, so all three run the same sanity pass.
func recoverDirectory(path string) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

a second wal instance or an offline utility opens the same path while one is live may corrupts the log

can we add a FileLock to the dir?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added file locks

resp := w.startIterator(m.startIndex)
m.reply <- resp
if resp.err != nil {
w.fail(resp.err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Iterator construction failure bricks the entire write path, and hard-link support becomes a hard filesystem requirement

should we distinguish the different failure cases? like for MkdirAll/os.Link error, just return error instead of bricking the entire write path

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Iterator failures are no longer fatal for failure modes that do not lead to corrupted state.

}

// Write accumulates a set of changes for the given block number in memory.
func (w *stateWALImpl) Write(blockNumber uint64, cs []*proto.NamedChangeSet) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the nil changeset check is performed asynchronously in state_wal_serialization.go. As a result, Write() and potentially SignalEndOfBlock() can return successfully even when the input contains a nil changeset. The serialization goroutine discovers the error later and bricks the WAL, so the error surfaces at an unrelated subsequent operation.

can we validate nil entries synchronously in Write() ? so the invalid input is rejected at the call site without bricking the WAL.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

change made

Comment thread sei-db/seiwal/seiwal_impl.go
seidroid[bot]

This comment was marked as low quality.

Comment thread sei-db/seiwal/seiwal_offline.go Outdated
Comment thread sei-db/seiwal/seiwal_impl.go
Comment thread sei-db/seiwal/seiwal_impl.go
Comment thread sei-db/seiwal/seiwal_impl.go
Comment thread sei-db/state_db/statewal/state_wal_impl.go

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ed97f93. Configure here.

Comment thread sei-db/seiwal/seiwal_serializing.go

@seidroid seidroid 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.

A well-documented, thoroughly tested new WAL utility package (seiwal) and a statewal wrapper; the code is not yet integrated and is carefully engineered for crash durability and recovery. The only code finding is a low-severity uint64 overflow in the contiguous-index validation.

Findings: 0 blocking | 5 non-blocking | 1 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Cursor's review (cursor-review.md) is empty — that second-opinion pass produced no output.
  • REVIEW_GUIDELINES.md (from the base branch) is empty, so no repo-specific review standards were applied.
  • seiwal_iterator.go:290 acknowledges (in a comment) that locating the start index is a linear scan over a fully-loaded file; fine for now, but worth revisiting if startup replay from a deep offset becomes a hotspot, as the comment notes.
  • Overall this is high-quality, self-contained utility code with strong test coverage; the main integration/correctness risk will land in the follow-up PRs that wire it into SS/SC.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

"append rejected: index %d is not greater than last appended index %d",
index, w.lastAppendIndex)
}
} else if index != w.lastAppendIndex+1 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] uint64 overflow edge case in the contiguous check: when lastAppendIndex == math.MaxUint64, lastAppendIndex+1 wraps to 0, so an Append(0, ...) is incorrectly accepted as "contiguous", producing a record with an inverted index relative to the file's existing max. The writer-side backstop at appendRecord (line 489, m.index != w.lastWrittenIndex+1) has the identical wrap. Reaching it only requires the baseline (first) append to use a near-max index rather than 2^64 total records. Realistically block numbers won't approach 2^64, so severity is low, but a cheap explicit guard (reject once the max index is reached) in both the caller-side and writer-side checks would harden it. (Also raised by Codex.)

@claude claude 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.

Beyond the inline findings, I also checked two other candidate issues and ruled them out: walImpl.Close()'s per-call closeErr local means a second Close() call after a failed first one would return nil — but Close() is only ever expected to be called once, so this isn't reachable in practice. And walImpl.Append advancing lastAppendIndex/hasAppended before sendToWriter succeeds is fine because sendToWriter only fails once the WAL is already closed or bricked, at which point no retry could succeed anyway.

Extended reasoning...

This run's two inline findings (the serializing/StateWAL layers still bricking on non-fatal iterator snapshot errors that walImpl itself now tolerates, and a narrow EOF-vs-shutdown race in the iterator) are the substantive output of this pass. I additionally reviewed the two candidate issues surfaced by finder agents and confirmed they are not real bugs, as noted in the message.

Comment thread sei-db/seiwal/seiwal_serializing.go
Comment thread sei-db/seiwal/seiwal_iterator.go
Comment thread .gitignore
sei-db/ledger_db/block/blocksim/bin/
sei-db/db_engine/litt/bin/ No newline at end of file
sei-db/db_engine/litt/bin/
sei-db/seiwal/walsim/bin/ No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it seems there is no such folder

w.hasAppended = true

record := frameRecord(index, data)
if err := w.sendToWriter(dataToBeWritten{record: record, index: index}); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what happens if the w.sendToWriter() fails but lastAppendIndex and hasAppended already advances. should we move w.lastAppendIndex = index / w.hasAppended = true before w.sendToWriter?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants