Skip to content

move pebble checkpoints off of the execution goroutine - #4039

Merged
cody-littley merged 4 commits into
mainfrom
cjl/background-snapshots
Aug 28, 2026
Merged

move pebble checkpoints off of the execution goroutine#4039
cody-littley merged 4 commits into
mainfrom
cjl/background-snapshots

Conversation

@cody-littley

Copy link
Copy Markdown
Contributor

Describe your changes and provide context

Moves pebble checkpointing (what we call snapshots) off of the transaction execution goroutine

@cursor

cursor Bot commented Aug 27, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes core state-commit durability, commit latency/backpressure, and snapshot/GC concurrency on the FlatKV path; mis-tuned lag or writer failures brick commits via a latched fatal error.

Overview
Moves FlatKV Pebble checkpointing off the commit/execution path by introducing a background SnapshotWriter that takes snapshots on a cadence, clones snapshots for read-only views, and performs collector-driven pruning as the sole mutator of the on-disk snapshot tree.

Commit now Offers each sealed block to the writer instead of calling synchronous WriteSnapshot; FlushSnapshots() waits for queued work when tests or tooling need disk to match committed height. Public WriteSnapshot is removed in favor of quiesced outOfBandSnapshot for import and initial-version bootstrap only.

Adds state-commit.flatkv.max-snapshot-lag-blocks (default 64): when the writer’s queue is full, Commit blocks to cap memory from blocks pinned behind a slow snapshot. Wiring includes GetConfig, fuzz/golden key lists, and flatkv_snapshot_queue_depth metrics.

Also bumps default MaxUnflushedVersions from 4 → 128 for FlatKV view managers, documents async PruneSnapshots, and hardens integration tests (state-store catch-up helpers, FlushSnapshots in suites that assumed synchronous snapshots).

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

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 28, 2026, 6:34 PM

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.46014% with 77 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.62%. Comparing base (bafdcf3) to head (ef4687b).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
sei-db/state_db/sc/flatkv/snapshot_writer.go 85.53% 26 Missing and 8 partials ⚠️
sei-db/state_db/sc/flatkv/snapshot.go 70.23% 16 Missing and 9 partials ⚠️
sei-db/state_db/sc/flatkv/store.go 87.75% 4 Missing and 2 partials ⚠️
...-db/state_db/sc/flatkv/snapshot_writer_messages.go 75.00% 3 Missing and 1 partial ⚠️
sei-db/state_db/sc/flatkv/store_gc.go 90.90% 2 Missing and 1 partial ⚠️
sei-db/state_db/sc/flatkv/store_write.go 72.72% 2 Missing and 1 partial ⚠️
sei-db/state_db/sc/flatkv/importer.go 0.00% 0 Missing and 1 partial ⚠️
sei-db/state_db/sc/flatkv/store_meta.go 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4039      +/-   ##
==========================================
- Coverage   61.22%   60.62%   -0.60%     
==========================================
  Files        2153     2079      -74     
  Lines      188393   182423    -5970     
==========================================
- Hits       115351   110602    -4749     
+ Misses      62298    61677     -621     
+ Partials    10744    10144     -600     
Flag Coverage Δ
sei-chain-pr 54.60% <100.00%> (?)
sei-db 69.80% <ø> (ø)
sei-db-state-db ?
sei-db-state-db-pr 72.88% <82.33%> (?)

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

Files with missing lines Coverage Δ
sei-cosmos/server/config/config.go 97.81% <100.00%> (+0.01%) ⬆️
sei-db/db_engine/view/view_manager_config.go 94.87% <100.00%> (ø)
sei-db/state_db/sc/flatkv/config/config.go 74.07% <100.00%> (-2.98%) ⬇️
...db/state_db/sc/flatkv/config/flatkv_test_config.go 100.00% <100.00%> (ø)
sei-db/state_db/sc/flatkv/metrics.go 96.07% <ø> (+0.42%) ⬆️
sei-db/state_db/sc/flatkv/store_apply.go 83.87% <100.00%> (-1.42%) ⬇️
sei-db/state_db/sc/flatkv/importer.go 89.15% <0.00%> (-2.11%) ⬇️
sei-db/state_db/sc/flatkv/store_meta.go 77.62% <0.00%> (-2.02%) ⬇️
sei-db/state_db/sc/flatkv/store_gc.go 84.78% <90.90%> (+0.08%) ⬆️
sei-db/state_db/sc/flatkv/store_write.go 73.88% <72.72%> (-3.14%) ⬇️
... and 4 more

... and 96 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.

Comment thread sei-db/state_db/sc/flatkv/store_write.go
Comment thread sei-db/state_db/sc/flatkv/snapshot_writer.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.

Moving FlatKV pebble checkpointing onto a dedicated SnapshotWriter goroutine is well-structured: reservation ownership is handed off cleanly at every path (written/declined/failed/discarded), failures are latched and re-surfaced on the commit path, and teardown drains an in-flight checkpoint before the databases close. Two non-blocking issues: the new max-snapshot-lag-blocks key is declared configurable but never read, and the 256× increase in MaxUnflushedVersions raises an in-memory backlog that has no byte-based bound.

Findings: 0 blocking | 2 non-blocking | 2 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • None at the file/PR level.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

// reach disk until it completes, and each one is retained in memory meanwhile. This bounds how far
// that can run, trading a pause in block production for the memory the backlog would otherwise
// consume. It bounds blocks rather than bytes, so it mitigates exhaustion rather than preventing it.
MaxSnapshotLagBlocks uint32 `mapstructure:"max-snapshot-lag-blocks"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] The mapstructure:"max-snapshot-lag-blocks" tag is inert: neither sei-cosmos/server/config.GetConfig (which explicitly reads five other state-commit.flatkv.* keys — fsync, async-write-buffer, snapshot-interval, snapshot-keep-recent, enable-read-write-metrics) nor app/parseSCConfigs reads this key, and the Viper path ignores unknown TOML keys. An operator who sets state-commit.flatkv.max-snapshot-lag-blocks in app.toml gets silence, and the queue stays pinned at 512.

This knob is the whole of the writer's backpressure and the one lever an operator has when a checkpoint outruns block production, so it is the flatkv field most worth wiring rather than least. Suggest adding the guarded read alongside the other four in GetConfig, and a row in sei-cosmos/server/config/config_fuzz_test.go ({Key: "state-commit.flatkv.max-snapshot-lag-blocks", Path: "StateCommit.FlatKVConfig.MaxSnapshotLagBlocks", Set: ...}) per testutil/configtest/AGENTS.md. If it is deliberately not operator-tunable for now, dropping the tag (as ExternalPruning does with mapstructure:"-") records that decision instead of implying one that does not hold.

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

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.

fixed

Comment thread sei-db/db_engine/view/view_manager_config.go Outdated
MetricsEnabled: true,
MetricsScrapeIntervalSeconds: 10,
MaxUnflushedVersions: 4,
MaxUnflushedVersions: 1024,

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.

any OOM risk here to MaxUnflushedVersions to 1024?

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.

Changed this to 128. In benchmarking, I observed 1024 was safe. But probably not necessary to be quite as big as that.

// Step 5: Offer the block to the snapshot writer, which decides whether it becomes a snapshot and,
// if so, writes it on its own goroutine. Periodic snapshots are what keep the WAL bounded and
// restarts fast.
if s.snapshotWriter != 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.

Offer now reports the writer's latched failure, so the error names the current height, not the height whose snapshot failed. The halt is also one block late. Both are acceptable if nothing downstream reads that height for recovery. Please confirm, and consider naming the failed height in the latched error so the log points at the real block.

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.

Error currently reports both the block that failed and the block where the error bubbles up. Nothing should be attempting to recover this, the block number is for debugging only.

// cadence is outrunning.
w.phaseTimer.SetPhase("idle")
select {
case <-w.ctx.Done():

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.

select randomly chooses among ready cases, so shutdown competes with the queue instead of taking priority. Once stop() is called, w.ctx.Done() remains ready, causing races on both sides of the channel.

Here, a queued message may win, causing Close to wait for another snapshot and breaking its guarantee that queued work is discarded.

In enqueue at L166, a send may win if the queue has room, so Offer returns nil even though the writer may exit without processing it. Commit then reports success for a snapshot that may never be written.

Both sides need to be fixed: one prevents extra work during shutdown; the other prevents false success after shutdown.

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.

IMO, not a problem if shutdown loses the race. Even if shutdown was guaranteed to win in the select statement, we might receive a shutdown one nanosecond after we start a pebble checkpoint... so it's still possible that shutdown might be delayed by a pebble checkpoint.

Also mitigating this problem is that in production use cases, we don't really shut down a store so much as pull the rug out from under it... so I don't think a slow shutdown really hurts us much.

Comment thread app/testdata/state-commit.golden Outdated
FlatKVConfig.AccountStoreConfig.MetricsEnabled = bool(true)
FlatKVConfig.AccountStoreConfig.MetricsScrapeIntervalSeconds = float64(10)
FlatKVConfig.AccountStoreConfig.MaxUnflushedVersions = uint64(4)
FlatKVConfig.AccountStoreConfig.MaxUnflushedVersions = uint64(1024)

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.

This seems to be a pretty big jump? Why do we need that big of backlog?

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.

It was helpful during performance testing to absorb bursty behavior. 1024 is larger than it needs to be though, after I did several optimizations. Reduced to 128.

@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 ef4687b. Configure here.

case <-w.ctx.Done():
return fmt.Errorf("clone snapshot for version %d: %w", targetVersion, w.stoppedError())
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Clone aborts while copy still runs

Medium Severity

CloneSnapshot returns as soon as the writer context is cancelled, even after the clone is already queued or running. LoadVersionReadOnly then treats that as failure and Close deletes readOnlyWorkDir while createWorkingDir may still be copying into it. That is the documented Close-during-export window: the copy can recreate or leak a readonly-* directory, or observe a half-deleted dest, instead of finishing and failing later on WAL close.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ef4687b. Configure here.

@cody-littley
cody-littley enabled auto-merge August 28, 2026 19:04
@cody-littley
cody-littley added this pull request to the merge queue Aug 28, 2026
Merged via the queue into main with commit 257452c Aug 28, 2026
114 of 116 checks passed
@cody-littley
cody-littley deleted the cjl/background-snapshots branch August 28, 2026 19:33
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