Skip to content

Add global checkpoint scheduler - #4026

Open
yzang2019 wants to merge 7 commits into
mainfrom
yzang/snapshot-scheduler
Open

Add global checkpoint scheduler#4026
yzang2019 wants to merge 7 commits into
mainfrom
yzang/snapshot-scheduler

Conversation

@yzang2019

@yzang2019 yzang2019 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Describe your changes and provide context

Two main changes in this PR:

  1. Move existing SS checkpointer to ss/snapshot
  2. Create a new global checkpoint scheduler and put that under controller

This PR introduce the interface of new global scheduler, which is not reachable from production yet, nothing implements it. A follow-up PR will wire it up with existing stores.

Testing performed to validate your change

@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches checkpoint/snapshot coordination paths used by SS stores, but production still uses the composite snapshot coordinator; the new global scheduler’s missed-completion stall behavior could affect nodes once wired.

Overview
Introduces a node-wide checkpoint coordinator in controller: stores call ShouldCheckpoint on every commit and MarkCheckpointComplete when done, so every SS/SC member checkpoints the same block height under configurable wall-clock and block intervals (config.CheckpointConfig, default aligned with memiavl snapshot cadence). The scheduler holds a height until all registered stores report, rejects split decisions across stores, and can stall entirely if a store skips completion.

Refactor: Per-engine checkpoint orchestration (ScheduleCheckpoint, FanIn, SupportsCheckpoint, ErrCheckpointCanceled) moves from controller to state_db/ss/snapshot as the Checkpointer interface. SS snapshot Manager and cosmos/EVM/composite callers now use Checkpointer instead of the old Scheduler field; behavior of existing SS online snapshots is unchanged at the call sites.

The new global scheduler is test-only for now—no production wiring in this PR (follow-up expected).

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

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.76106% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.33%. Comparing base (7cf5a21) to head (60fe243).

Files with missing lines Patch % Lines
sei-db/state_db/ss/snapshot/checkpoint.go 72.50% 7 Missing and 4 partials ⚠️
sei-db/config/checkpoint_config.go 0.00% 4 Missing ⚠️
sei-db/state_db/ss/evm/store.go 42.85% 3 Missing and 1 partial ⚠️
sei-db/state_db/ss/snapshot/manager.go 42.85% 1 Missing and 3 partials ⚠️
sei-db/state_db/ss/composite/snapshot.go 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4026      +/-   ##
==========================================
- Coverage   61.25%   60.33%   -0.92%     
==========================================
  Files        2153     2069      -84     
  Lines      188426   178153   -10273     
==========================================
- Hits       115414   107483    -7931     
+ Misses      62274    60785    -1489     
+ Partials    10738     9885     -853     
Flag Coverage Δ
sei-chain-pr 70.42% <92.45%> (?)
sei-db 69.80% <ø> (ø)
sei-db-state-db ?
sei-db-state-db-pr 67.42% <66.66%> (?)

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

Files with missing lines Coverage Δ
sei-db/controller/checkpoint_scheduler.go 100.00% <100.00%> (+25.00%) ⬆️
sei-db/db_engine/types/types.go 50.00% <ø> (+20.00%) ⬆️
sei-db/state_db/ss/cosmos/store.go 82.43% <100.00%> (ø)
sei-db/state_db/ss/composite/snapshot.go 83.43% <50.00%> (ø)
sei-db/config/checkpoint_config.go 0.00% <0.00%> (ø)
sei-db/state_db/ss/evm/store.go 58.11% <42.85%> (ø)
sei-db/state_db/ss/snapshot/manager.go 56.80% <42.85%> (ø)
sei-db/state_db/ss/snapshot/checkpoint.go 72.50% <72.50%> (ø)

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

@github-actions

github-actions Bot commented Aug 26, 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, 5:24 AM

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

Comment thread sei-db/controller/checkpoint_scheduler.go Outdated

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

Clean relocation of the SS checkpoint helpers into ss/snapshot plus a new global CheckpointScheduler; the move is complete and consistent, and the scheduler is thoroughly unit-tested. The scheduler is not yet wired into production, and its store contract leaves checkpoint completion inferred rather than reported, which is worth pinning down before implementations land.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] The new global scheduler is not reachable from production: nothing implements CheckpointableStore (grep for CheckpointInProgress finds no implementation outside sei-db/controller), and NewCheckpointScheduler is called only from checkpoint_scheduler_test.go. SS still constructs the existing per-composite coordinator in sei-db/state_db/ss/composite/snapshot.go, so this PR adds the mechanism without enabling global checkpoint scheduling. Fine as a staged change — worth stating in the description that a follow-up wires it up, so it is not mistaken for a live feature.
  • [suggestion] CheckpointableStore gives a store no way to report that a checkpoint failed. A failed checkpoint clears CheckpointInProgress() and the store's version still advances past the target, so the scheduler reads it as a success: it starts the min-time gate and moves to the next boundary. Combined with the fact that only dispatch is logged (checkpoint scheduled), a node can silently stop producing usable checkpoints while the scheduler reports healthy activity. Consider a completion/error signal on the interface, or at minimum a failure counter the stores increment.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-db/controller/checkpointable_store.go Outdated
Comment thread sei-db/controller/checkpoint_scheduler.go Outdated
@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 relocation of the SS checkpoint helpers into ss/snapshot and the SchedulerCheckpointer rename are complete and consistent (no stale references to the removed controller symbols remain), and the new global CheckpointScheduler is well tested. Two non-blocking issues remain in the scheduler: a dispatch that can be silently dropped by a store yet counted as a completed checkpoint, and a first cycle that runs before shutdown/cancellation is observed.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] checkpointPollInterval is a hard-coded 10s while the rest of the cadence is configurable, and CheckpointConfig.Validate does not relate the two. The scheduler can therefore dispatch at most one boundary per 10s, so a small CheckpointInterval (or fast blocks) silently yields a longer effective cadence than configured. Worth either validating/warning when the configured interval is short relative to the poll rate, or deriving the poll interval from the config.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-db/controller/checkpoint_scheduler.go Outdated
Comment thread sei-db/controller/checkpoint_scheduler.go Outdated
Comment thread sei-db/controller/checkpoint_scheduler.go Outdated
Comment thread sei-db/controller/checkpoint_scheduler.go Outdated
Comment thread sei-db/controller/checkpoint_scheduler.go
// scheduleNextCheckpoint runs one cycle: it hands the next boundary to every store, or does nothing
// when a store is still writing the last checkpoint, has not committed past its scheduled version, or
// the last one finished too recently.
func (s *CheckpointScheduler) scheduleNextCheckpoint() {

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.

One nice to have property would be if this utility ensured that different checkpoint stores checkpoint the same block number. Not required for correctness of course, we're intentionally designing a system that doesn't break when checkpoints don't align (since a crash can cause us to be missing some checkpoints, even if we intend to checkpoint the same block). But its still nice to align them when we can.

If we wanted this property, we'd have to flip the model a little. Instead of the scheduler saying "it's time to checkpoint your current block", we'd have each of the stores ask the scheduler "should I checkpoint block X?". Via this mechanism we could ensure that we don't have an off-by-one block height difference when we snapshot our different stores.

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 to a poll model, but poll model has its own edge case and limitation as well.

@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 controllerss/snapshot relocation is complete and consistent (no stale controller.* checkpoint references remain, and the SchedulerCheckpointer rename is applied everywhere), and the redesigned pull-based CheckpointScheduler is well documented and thoroughly tested. Two non-blocking issues concern the baseline used before the first checkpoint, which makes the two intervals behave asymmetrically across restarts.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] A store registers by asking and there is no way to remove it, so a store that asks once and then stops asking — closed, disabled by a mode switch, or its commit loop wedged — is cloned into awaiting at the next pick and stops checkpointing for the whole node permanently. The only log on the path is checkpoint complete, so this is indistinguishable from an idle node. Since nothing implements the caller side yet, either an explicit deregistration call or a periodic warn naming the stores a held height is still waiting on is cheap to add before the follow-up wires real stores up.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

config: cfg,
registered: make(map[string]struct{}),
awaiting: make(map[string]struct{}),
checkpointedAt: time.Now(),

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] Seeding checkpointedAt with time.Now() restarts the time interval on every process start, so a node that restarts more often than TimeInterval never checkpoints at all. With DefaultCheckpointConfig that is a 1-hour window: a node cycling every ~50 minutes produces no checkpoints and logs nothing beyond checkpoint scheduler created.

The analogous memiavl path deliberately avoids this — db.go:283 seeds lastSnapshotTime from the existing snapshot directory's mtime rather than from process start (getSnapshotModTime). Since DefaultCheckpointConfig is documented as mirroring those settings, consider giving the scheduler the same recovery: seed checkpointedAt from the newest existing checkpoint when the caller can supply one, rather than unconditionally from construction time.

// hasReachedNextInterval reports whether every configured interval has passed for version.
func (s *CheckpointScheduler) hasReachedNextInterval(version int64) bool {
timeElapsed := s.config.TimeInterval <= 0 || time.Since(s.checkpointedAt) >= s.config.TimeInterval
blocksElapsed := s.config.BlockInterval <= 0 || version-s.nextCheckpointVersion >= s.config.BlockInterval

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 block gate is measured from nextCheckpointVersion, which is 0 until the first height is picked, so before the first checkpoint it is effectively version >= BlockInterval — trivially true for any live chain. With a block-only config (TimeInterval unset), the very first version any store commits after a restart is therefore always a checkpoint height, whatever the node's height. The type doc at line 33 says both intervals are "measured from ... the scheduler's creation before there is one", which for the block interval would mean the height at creation; 0 is not that.

Recording a baseline version on the first ask (or, like checkpointedAt, seeding it at construction) would make the two gates behave the same way before the first checkpoint and match the documented contract. Worth noting that this and the checkpointedAt seeding are opposite failure modes of the same missing "initial baseline": the block gate never restricts, the time gate always restricts.

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