Skip to content

FlatKV: background read/fold for account data - #4155

Merged
cody-littley merged 5 commits into
mainfrom
cjl/background-account-reads-2
Sep 15, 2026
Merged

cody-littley merged 5 commits into
mainfrom
cjl/background-account-reads-2

Conversation

@cody-littley

Copy link
Copy Markdown
Contributor

Describe your changes and provide context

Move account read/fold workflow off of the execution thread.

@cody-littley cody-littley self-assigned this Sep 11, 2026
@cody-littley cody-littley changed the title Cjl/background account reads 2 FlatKV: background read/fold for account data Sep 11, 2026
@cursor

cursor Bot commented Sep 11, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Large MVCC/shard lifecycle changes (staging, latches, diff freeze, shutdown ordering) sit on the state-commit path; fold or corruption failures now surface asynchronously and can brick the view manager.

Overview
Adds BatchUpdate to the view engine: callers pass keys and a BatchUpdater, the manager stages unresolved values synchronously and folds them on background pool threads. Reads, iterators, and version diffs block on pending folds; fold failures brick the manager; Close waits for in-flight folds before tearing down the DB/pools.

FlatKV stops batch-reading account rows on the block-apply thread. Partial nonce/codehash/balance changes are parsed up front, then accountStore.BatchUpdate merges each row via accountUpdater.NewValueFor. The old synchronous read-merge path and flatkv_batch_read_old_values_latency metric are removed in favor of flatkv_account_update_latency.

Coverage includes dedicated batch-update tests, differential fuzz with opUpdate, and shutdown tests that folds must finish before close.

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

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 15, 2026, 11:51 AM

@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.71955% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.91%. Comparing base (9435e24) to head (4ec4270).

Files with missing lines Patch % Lines
sei-db/db_engine/view/shard.go 80.44% 53 Missing ⚠️
sei-db/db_engine/view/view_manager_impl.go 83.33% 6 Missing ⚠️
sei-db/db_engine/view/pending_value.go 88.88% 1 Missing ⚠️
sei-db/state_db/sc/flatkv/store_apply.go 97.22% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4155      +/-   ##
==========================================
- Coverage   66.69%   65.91%   -0.78%     
==========================================
  Files        2202     2105      -97     
  Lines      169563   160874    -8689     
==========================================
- Hits       113087   106044    -7043     
+ Misses      56335    54689    -1646     
  Partials      141      141              
Flag Coverage Δ
sei-chain-pr 89.20% <81.07%> (?)
sei-db 74.50% <ø> (ø)
sei-db-state-db ?
sei-db-state-db-pr 83.36% <97.22%> (?)

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

Files with missing lines Coverage Δ
sei-db/db_engine/view/read_cache.go 95.83% <ø> (+1.04%) ⬆️
sei-db/db_engine/view/shard_manager.go 92.85% <100.00%> (+0.54%) ⬆️
sei-db/state_db/sc/flatkv/metrics.go 97.29% <ø> (ø)
sei-db/db_engine/view/pending_value.go 88.88% <88.88%> (ø)
sei-db/state_db/sc/flatkv/store_apply.go 92.96% <97.22%> (+0.10%) ⬆️
sei-db/db_engine/view/view_manager_impl.go 90.41% <83.33%> (-0.59%) ⬇️
sei-db/db_engine/view/shard.go 85.13% <80.44%> (-4.45%) ⬇️

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

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

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d38fe4d. Configure here.

Comment thread sei-db/db_engine/view/view_manager_impl.go
seidroid[bot]
seidroid Bot previously requested changes Sep 11, 2026

@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 the account read/fold off the execution thread is well structured — staging under the shard lock, pending-value handles checked on every read path, and per-version latches that gate the diff consumers — and the new tests (including the differential opUpdate op) cover the staging/chaining/delete/failure semantics well. The blocking gap is lifecycle: the folds are fire-and-forget work that nothing drains, so Close can tear down the read pool and the database underneath an in-flight fold.

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

Blockers

  • None at the file/PR level.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • [suggestion] ApplyChangeSets no longer detects a corrupt stored account row — newAccountUpdater only parses the changeset, and deserialization of the row being folded onto now happens on a fold worker. The block is therefore accepted and the failure resurfaces later as a bricked manager (and a panic out of CommitStore.Get, as TestCrashRecoveryCorruptedAccountValueInDB now records) rather than as an error attributable to the apply. The node still halts before publishing a hash, so this is a change in failure surface rather than a correctness hole, but it is worth calling out explicitly in the CommitStore.ApplyChangeSets contract so operators reading a fold-failure brick know which block produced it.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-db/db_engine/view/view_manager_impl.go
Comment thread sei-db/db_engine/view/view_manager_impl.go Outdated
@cody-littley

Copy link
Copy Markdown
Contributor Author

@seidroid review

@cody-littley

Copy link
Copy Markdown
Contributor Author

@seidroid review

@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 the account fold off the execution thread is carefully built: staging under the shard lock, pending handles checked on every read surface, per-version latches gating the diff consumers, and a Close drain that closes the lifecycle gap from the previous review. Remaining notes are non-blocking: a residual shutdown window after a brick, an unenforced no-duplicate-keys contract that turns into a hang, and the fold's latency no longer being measured anywhere.

Findings: 0 blocking | 4 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] CommitStore.ApplyChangeSets's contract still describes the old failure surface. A corrupt stored account row used to fail the apply with unsupported serialization version attributable to the block that read it; now the apply succeeds and the failure resurfaces as a bricked manager at Commit (or a panic out of CommitStore.Get, as TestCrashRecoveryCorruptedAccountValueInDB now records). The node still halts before publishing a hash, so this is a change in failure surface rather than a correctness hole, but the ApplyChangeSets godoc is where an operator would look to map a fold-failure brick back to a block. This was raised on the previous review and the code still does not state it anywhere.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-db/db_engine/view/view_manager_impl.go
Comment thread sei-db/db_engine/view/view_manager_impl.go
Comment thread sei-db/state_db/sc/flatkv/store_apply.go
@seidroid
seidroid Bot dismissed their stale review September 15, 2026 11:52

Superseded: latest AI review found no blocking issues.

@cody-littley
cody-littley added this pull request to the merge queue Sep 15, 2026

@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 the account fold off the execution thread remains well built — staging under the shard lock, pending handles on every read surface, per-version latches gating the diff consumers, and a Close drain — and the head commit is unchanged since the last review, so the three previously unresolved suggestions still stand. No blockers; the notes are a duplicate-key hang, a drain whose godoc over-claims on the brick path, a metrics blind spot, and some comments left stale by the removed account reads.

Findings: 0 blocking | 6 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] Two comments in store_apply.go were left describing account reads this PR removed. applyChangeSets's lock rationale (line 36) still says the body "reads old values out of the stores and writes this block's values into them", and writeToStores's godoc (line 234) still says "The writes must come after the account reads in prepareWrites, because writing here is what makes this block's values visible to a read through the same store." prepareWrites no longer reads any account, so that ordering constraint no longer exists and the next reader will look for a read that isn't there. Rewrite both rather than patching them.
  • [suggestion] CommitStore.ApplyChangeSets's contract still describes the old failure surface. A corrupt stored account row used to fail the apply with unsupported serialization version attributable to the block that read it; now the apply succeeds and the failure resurfaces as a bricked manager at Commit (or a panic out of CommitStore.Get, as TestCrashRecoveryCorruptedAccountValueInDB now records). The node still halts before publishing a hash, so this is a change in failure surface rather than a correctness hole, but the ApplyChangeSets godoc is where an operator would look to map a fold-failure brick back to a block. Raised on both previous reviews; the code still does not state it anywhere.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.
  • 1 non-blocking pre-existing issue(s) listed below under pre-existing issues.

Pre-existing issues

  • [suggestion] readCache.ResolveBatchUnlocked (sei-db/db_engine/view/read_cache.go:368-375) abandons its drain on context cancellation without waiting for the reads it already submitted to readPool. This predates the PR — an ordinary BatchGet racing a brick hits the same window — and the inline note on awaitOutstandingFolds is about the new fold path inheriting it.

if len(work[shardIndex]) == 0 {
continue
}
folds, err := c.shards[shardIndex].StageUpdates(keys, work[shardIndex], version)

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] BatchUpdate's contract says "keys must not repeat", but nothing enforces it and a violation is unrecoverable rather than an error.

With keys = [k, k], both indices land in the same shard batch. StageUpdates stages P0 for the first; capturePriorValueWLocked then sees P0 as the newest entry and gives the second priorValueInEarlierFold{P0}. FoldStagedValues awaits P0 inside resolvePriorValuesUnlocked — the very call that would resolve it — so the fold never completes. The latch never opens, Commit/flush block in awaitVersionFoldsUnlocked, and Close blocks forever in AwaitOutstandingFolds, whose wait is deliberately not interruptible (and which runs before cancel(), so nothing wakes the parked await either).

The only caller today derives keys from a map, so it cannot produce a duplicate — but that is a convention the next caller can forget, where a check at the single function every path passes through is an invariant they cannot. A dedup check in StageUpdates (or in partitionIndicesByShard, which already walks the slice) returning an error would turn an unrecoverable hang into a failed call.

Still present at head; no commits since the previous review.

// closes once Close returns. Close calls this before cancelling, so that a fold in flight resolves
// against an open database instead of abandoning a read that would then race db.Close, and outside
// versionLock, which a failing fold takes to brick the manager.
func (c *viewManager) awaitOutstandingFolds() {

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 drain closes the clean-shutdown case, but the godoc's guarantee — "a fold in flight resolves against an open database instead of abandoning a read that would then race db.Close" — does not hold once the manager has bricked, because bricking cancels the context the fold's read is parked on.

With two folds in flight: fold A fails, FailStagedFolds releases A's latch and reportFoldFailurebrickcancel(). Fold B is inside readCache.ResolveBatchUnlocked, parked in InterruptiblePull; the cancel makes it bail without draining the reads it already submitted (read_cache.go:368-375). B then calls FailStagedFolds, releasing its latch, so awaitOutstandingFolds sees nothing outstanding and returns while B's readPool task is still queued or inside readFromDB.

Scoping this down from the previous review: in the production teardown this is contained, because CommitStore.Close closes readPool (a fixedPool, whose Close drains the queue and waits) before closeDBsOnly, and the abandoned task's valueChan is buffered so it cannot wedge a worker. So the read-after-close does not actually occur through that path. What is left is that this function's stated invariant is stronger than what it delivers, and it only holds because of an ordering the manager does not own. Either narrow the comment to the clean-shutdown case and name the readPool-before-DB dependency, or drain the fold-owned read tasks themselves rather than the latches, which a failure releases early.

if alreadyHave[accountDBDir] < version && prepared.accounts != nil {
start := time.Now()
err := s.accountStore.BatchUpdate(prepared.accounts.keys, prepared.accounts)
otelMetrics.AccountUpdateLatency.Record(s.ctx, secondsSince(start),

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] AccountUpdateLatency now times accountStore.BatchUpdate, which by design does no I/O and no folding — it is one lock hold per shard. The work this PR moved (the prior-value reads plus the merge) is no longer measured by anything: flatkv_batch_read_old_values_latency covered the account reads and is removed here.

That leaves a blind spot exactly where the new risk is. A fold that stalls on a slow database read now delays Commit (via awaitVersionFoldsUnlocked) and any read of a touched account, but the only visible symptom would be CommitLatency rising with no metric explaining why. Consider recording the fold duration where FoldStagedValues completes, or keeping a read-latency histogram on the prior-value path, so the moved cost stays attributable.

Merged via the queue into main with commit 1179506 Sep 15, 2026
125 of 126 checks passed
@cody-littley
cody-littley deleted the cjl/background-account-reads-2 branch September 15, 2026 12:15
masih pushed a commit that referenced this pull request Sep 15, 2026
## Describe your changes and provide context

Move account read/fold workflow off of the execution thread. Cherry pick
from main.

Co-authored-by: Cody Littley <cody.littley@seinetwork.io>
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.

2 participants