Skip to content

Add context cancellation to SS DB layer - #3940

Open
yzang2019 wants to merge 11 commits into
mainfrom
yzang/fix-trace-timeout
Open

Add context cancellation to SS DB layer#3940
yzang2019 wants to merge 11 commits into
mainfrom
yzang/fix-trace-timeout

Conversation

@yzang2019

Copy link
Copy Markdown
Contributor

Summary

debug_trace* can spend a long time inside a PebbleDB MVCC skip loop while still holding a trace semaphore slot. trace_timeout did not unwind that loop, so other traces failed immediately with server busy.

This change threads the RPC deadline into those skip loops so a timed-out trace aborts, releases its slot, and returns the deadline instead of a synthesized error trace.

  • Attach the RPC/trace context on the SDK context used for historical replay.
  • Wrap KV stores with ctxkv so Iterator / ReverseIterator forward that context (no-op when the context cannot be cancelled, so block execution is unchanged).
  • Add optional ContextIterator / ContextIteratorStore interfaces; Pebble skip loops poll ctx.Err() between steps.
  • Recover cancel/deadline panics in runTx as errors, and surface context.DeadlineExceeded from debug_traceTransaction / TraceBlockBy* / TraceCall.

A single in-flight Pebble Seek / Next still runs to completion. Abort happens between skip steps.

Test plan

  • Pebble iterator tests: cancelled constructor and Next panic
  • ctxkv forwards a cancellable context and is a no-op otherwise
  • resultUnlessExpired returns the deadline instead of a fake error trace
  • TestTraceTransactionTimeoutReleasesSemaphore: first debug_traceTransaction blocked in an SS-shaped skip loop times out; a concurrent call is rejected as busy; the semaphore slot is free afterwards

@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches core store iteration, baseapp panic recovery, and EVM historical trace paths; behavior is gated on cancellable contexts but incorrect wiring could affect long-running traces or snapshot cleanup.

Overview
Timed-out debug_trace* calls could keep running inside Pebble SS MVCC skip loops while holding a trace semaphore, so other traces saw server busy and timeouts surfaced as fake error traces or cache rows.

This PR threads the RPC/trace context into historical replay and store iteration: initializeBlock puts the deadline on the SDK context, ctxkv forwards it into IteratorWithContext (no-op when the context is not cancellable), and Pebble MVCC iterators poll ctx.Err() between skip steps. runTx recovers matching cancel/deadline panics as errors; resultUnlessExpired and initializeBlock panic recovery release snapshot leases and return context.DeadlineExceeded instead of synthesized traces. The trace baker uses the same guard so expired bakes are not cached.

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

@github-actions

github-actions Bot commented Aug 17, 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 19, 2026, 9:15 AM

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.00000% with 72 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.57%. Comparing base (e63fe54) to head (ee8aeff).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
sei-db/state_db/ss/evm/store.go 0.00% 14 Missing ⚠️
...i-db/db_engine/pebbledb/mvcc/iterator_ascending.go 48.00% 9 Missing and 4 partials ⚠️
sei-cosmos/store/ctxkv/store.go 54.16% 11 Missing ⚠️
sei-db/state_db/ss/composite/store.go 0.00% 8 Missing ⚠️
sei-db/db_engine/pebbledb/mvcc/iterator.go 82.85% 4 Missing and 2 partials ⚠️
sei-db/db_engine/types/types.go 37.50% 4 Missing and 1 partial ⚠️
evmrpc/simulate.go 88.23% 2 Missing and 2 partials ⚠️
sei-db/state_db/ss/cosmos/store.go 0.00% 4 Missing ⚠️
sei-cosmos/storev2/state/store.go 62.50% 3 Missing ⚠️
sei-cosmos/baseapp/recovery.go 86.66% 1 Missing and 1 partial ⚠️
... and 2 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3940      +/-   ##
==========================================
- Coverage   59.60%   58.57%   -1.03%     
==========================================
  Files        2329     2231      -98     
  Lines      199860   188594   -11266     
==========================================
- Hits       119117   110470    -8647     
+ Misses      69339    67683    -1656     
+ Partials    11404    10441     -963     
Flag Coverage Δ
sei-chain-pr 75.16% <76.88%> (?)
sei-db ?
sei-db-state-db ?
sei-db-state-db-pr 71.39% <0.00%> (?)

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

Files with missing lines Coverage Δ
evmrpc/trace_baker.go 76.74% <100.00%> (ø)
evmrpc/tracers.go 70.95% <100.00%> (+1.65%) ⬆️
sei-cosmos/baseapp/baseapp.go 77.48% <100.00%> (+0.04%) ⬆️
sei-cosmos/store/cachekv/store.go 87.50% <100.00%> (+0.05%) ⬆️
sei-cosmos/store/types/store.go 77.27% <100.00%> (+3.58%) ⬆️
sei-db/db_engine/pebbledb/mvcc/db.go 69.53% <100.00%> (+0.18%) ⬆️
sei-db/db_engine/pebbledb/mvcc/db_ascending.go 63.42% <100.00%> (ø)
evmrpc/trace_profile.go 65.93% <0.00%> (ø)
sei-cosmos/types/context.go 90.87% <75.00%> (ø)
sei-cosmos/baseapp/recovery.go 86.44% <86.66%> (+0.07%) ⬆️
... and 9 more

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

seidroid[bot]
seidroid Bot previously requested changes Aug 17, 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.

The context-threading design (ctxkv wrapper, optional ContextIterator/ContextIteratorStore interfaces, Pebble skip-loop polling) is coherent and the no-op-when-uncancellable guard keeps block execution off the new path. The blocking problem is that TraceBaker calls tracersAPI.TraceBlockByNumber directly with a cancellable timeout context, bypassing the new resultUnlessExpired guard, so a bake timeout can persist a synthesized error trace into the trace cache.

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

Blockers

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

Non-blocking

  • Every cancellation now flows through x/evm/keeper/msg_server.go:84, which re-panics but first calls debug.PrintStack(), logs EVM PANIC, and increments the panics counter / evm_panics_total. A routine trace_timeout will therefore dump a stack per replayed tx and move a panic-alert metric. Consider special-casing context.Canceled/context.DeadlineExceeded there alongside the existing ErrReadEstimate exemption.
  • Context.GigaKVStore (sei-cosmos/types/context.go:575) is not ctxkv-wrapped, unlike KVStore and TransientStore, so Giga-backed reads will not observe the trace deadline. Likely intentional (Giga is the block-execution path, not trace replay), but the asymmetry is worth a one-line comment.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread evmrpc/tracers.go
Comment thread evmrpc/simulate.go
Comment thread sei-db/db_engine/pebbledb/mvcc/iterator.go
Comment thread sei-db/db_engine/pebbledb/mvcc/iterator.go
Comment thread sei-db/db_engine/rocksdb/mvcc/db.go
Comment thread sei-cosmos/baseapp/recovery.go Outdated

// newContextCancelledRecoveryMiddleware recovers a store access that panicked
// because the caller's context was cancelled or exceeded its deadline.
func newContextCancelledRecoveryMiddleware(next recoveryMiddleware) recoveryMiddleware {

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.

medium: newContextCancelledRecoveryMiddleware runs for every runTx mode, DeliverTx included, and it is checked first. Any panic whose value wraps context.Canceled/DeadlineExceeded — from any source, not only the new iterator — now returns code 1 / codespace undefined instead of ErrPanic

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.

Good find, fixed

@yzang2019

Copy link
Copy Markdown
Contributor Author

@seidroid review

seidroid[bot]
seidroid Bot previously requested changes Aug 17, 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.

The cancellation plumbing (ctxkv wrapper, optional ContextIterator/ContextIteratorStore, Pebble skip-loop polling, runTx recovery, resultUnlessExpired) is coherent and well tested, and the trace-baker cache-poisoning and finishMVCCIterator blast-radius issues from the last round are fixed. The remaining blocker is unchanged: the cancellable context still reaches legacyabci.BeginBlock with no recovery, so a trace/bake timeout during BeginBlock unwinds as a panic and leaks the memiavl snapshot lease.

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

Blockers

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

Non-blocking

  • Every cancellation panic raised during replay flows through x/evm/keeper/msg_server.go:84, which calls debug.PrintStack(), logs EVM PANIC, and increments the panics counter / evm_panics_total before re-panicking (only occtypes.ErrReadEstimate is exempted). Because this PR makes trace timeouts a routine, expected event, a single timed-out debug_traceBlockBy* on a busy block will dump one stack trace per replayed tx and move a panic-alert metric. Exempting context.Canceled / context.DeadlineExceeded there alongside the existing ErrReadEstimate check would keep the signal clean. (Also raised by Codex; flagged in the previous review and not yet addressed.)
  • Context.GigaKVStore (sei-cosmos/types/context.go:575) is still not ctxkv-wrapped while KVStore and TransientStore now are. Likely intentional (Giga is the block-execution path, not trace replay), but the asymmetry deserves a one-line comment so the next reader does not assume it was missed.
  • The new helpers take context.Context as the second parameter (types.IteratorOn(store, ctx, ...), seidbtypes.IterateWithContext(store, ctx, ...), ctxkv.Wrap(parent, ctx)), against the Go convention of ctx first. These are new exported APIs, so it is cheap to fix now and awkward later.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread evmrpc/simulate.go
Comment thread sei-db/db_engine/rocksdb/mvcc/db.go
Comment thread sei-db/db_engine/pebbledb/mvcc/db.go
@yzang2019

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.

The initializeBlock blocker from the previous round is genuinely fixed: the base/next snapshot leases are now released on a cancel/deadline panic, other panics are re-raised after release, and both behaviours are covered by tests. The cancellation plumbing (ctxkv wrapper, optional ContextIterator/ContextIteratorStore, Pebble skip-loop polling, runTx recovery, resultUnlessExpired including the trace baker) reads coherently and its blast radius stays confined to the debug_trace* / baker paths; what remains is non-blocking cleanup plus two nits carried over unaddressed.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • x/evm/keeper/msg_server.go:84 still calls debug.PrintStack(), logs EVM PANIC, and increments the panics counter / evm_panics_total before re-panicking, with only occtypes.ErrReadEstimate exempted. This PR makes trace timeouts a routine event, so one timed-out debug_traceBlockBy* on a busy block dumps a stack trace per replayed tx and moves a panic-alert metric — and a bake-enabled node behind the tip does this continuously. Exempting context.Canceled / context.DeadlineExceeded alongside the existing check keeps the alert signal clean. (Also raised by Codex; third round unaddressed.)
  • The profiled block-trace worker goroutines (evmrpc/block_trace_profiled.go:197-220) have defer pend.Done() but no panic recovery, and api.backend.GetBlockContext(...) on line 208 runs outside profiledTraceTx's recover. Now that SS iteration panics on cancellation, an unrecovered panic in one of those goroutines would take the process down rather than fail the trace. Today those calls only do point Gets (no skip loop), so this is latent rather than live — but a defer func() { if r := recover(); r != nil { results[task.index] = &tracers.TxTraceResult{...} } }() in the worker would make it safe by construction.
  • Context.GigaKVStore (sei-cosmos/types/context.go:575) is still not ctxkv-wrapped while KVStore and TransientStore now are. Likely intentional (Giga is the block-execution path, not trace replay), but the asymmetry still deserves a one-line comment so the next reader does not assume it was missed.
  • The new exported helpers still take context.Context as the second parameter (types.IteratorOn(store, ctx, …), seidbtypes.IterateWithContext(store, ctx, …), ctxkv.Wrap(parent, ctx)), against the Go convention of ctx first. These are new APIs, so it is cheap to fix now and awkward later.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

return NewRocksDBIterator(itr, readOpts, prefix, start, end, version, db.earliestVersion, true), nil
}

func (db *Database) IteratorWithContext(_ context.Context, storeKey string, version int64, start, end []byte) (dbm.Iterator, error) {

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] Still open from the previous round. These two methods make *Database satisfy types.ContextIteratorStore while discarding ctx, so IterateWithContext will prefer them and cancellation silently no-ops on rocksdb. The fallback branch in IterateWithContext already produces identical behaviour without them, so either drop them (making the fallback explicit) or add a comment stating that the rocksdb iterator has no cancellable skip loop — otherwise the interface's "can observe a deadline" contract is misleading.

// ReverseIterator dispatches between descending- and ascending-mode
// implementations depending on the on-disk encoding detected at open time.
func (db *Database) ReverseIterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) {
return db.ReverseIteratorWithContext(context.Background(), storeKey, version, start, end)

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] Still open from the previous round. The existing godoc on ReverseIterator ("dispatches between descending- and ascending-mode implementations depending on the on-disk encoding detected at open time") was deleted rather than moved, and the four *WithContext methods added here have none. Iterator above (line 542) kept its doc, so the file now documents one half of the pair. Restoring the ReverseIterator comment and giving IteratorWithContext/ReverseIteratorWithContext a one-line "what" would match the convention in AGENTS.md.

return db.ReverseIterator(EVMStoreKey, version, start, end)
}

func (s *EVMStateStore) IteratorWithContext(ctx context.Context, _ string, version int64, start, end []byte) (dbm.Iterator, error) {

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 separateDBs / routeKey decision is now duplicated between Iterator/ReverseIterator (lines 108-128) and these two new methods — four copies of the same routing rule that can drift independently. CompositeStateStore has the same duplication of evmRouted.

Elsewhere in this PR the non-context entry point delegates instead (pebbledb.Database.IteratorIteratorWithContext(context.Background(), …), and the same in cachekv.Store and storev2/state.Store). Doing that here keeps the routing decision at a single choke point:

func (s *EVMStateStore) Iterator(storeKey string, version int64, start, end []byte) (dbm.Iterator, error) {
	return s.IteratorWithContext(context.Background(), storeKey, version, start, end)
}

@seidroid
seidroid Bot dismissed stale reviews from themself August 18, 2026 00:19

Superseded: latest AI review found no blocking issues.

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

Comment thread evmrpc/tracers.go
Comment thread evmrpc/initialize_block_test.go Outdated
@yzang2019
yzang2019 added this pull request to the merge queue Aug 18, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 18, 2026
@masih masih added the backport release/v6.6 Backport to release v6.6 label Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants