GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Keep only the newest checkpoint on storage by blp · Pull Request #7905 · feldera/feldera

Conversation

@blp

@blp blp commented Nov 10, 2026

Copy link
Copy Markdown
Member

A pipeline kept its two newest checkpoints on storage. The older one is not needed on a single host: the newest checkpoint lists every batch that the pipeline used when it wrote it, so the older checkpoint's other files are unused. With a long delay between checkpoints, the older checkpoint could use as much storage again as the pipeline's state.

Commits

  1. Keep only the newest checkpoint on a single host (Fixes [dbsp] Keeping two checkpoints around is wasteful #7035).

    • Checkpointer::gc_checkpoint(except, multihost: bool) decides how many checkpoints to keep: 1 for a single host, 2 for a host of a multihost pipeline. The count is now private, in gc_keeping, instead of the old MIN_CHECKPOINT_THRESHOLD.
    • DBSPHandle and the CircuitDriver engine trait take multihost too, and the controller passes it after each checkpoint.
    • A multihost host still keeps 2, because a restart resumes from the newest checkpoint that every host has.
  2. Delete a multihost host's older checkpoint once every host has the newest.

    • New host endpoint POST /coordination/checkpoint/complete {"uuid": ...} and new gc_checkpoints_before(except, uuid), which keeps uuid and every newer checkpoint.
    • The request names the checkpoint, so that a late request cannot delete a checkpoint that another host still lacks. If there is no such checkpoint, the host logs a warning and does nothing.
    • The coordinator side is a separate PR in the cloud repo. Until it is deployed, multihost behaves as before (keeps 2).

Testing

Test Checks Fails when
gc_keep_one_then_restore (dbsp) Keep 1 after each of two back-to-back checkpoints, which share all batch files, then restore and compare output GC deletes files that the kept checkpoint shares with the old one
gc_commits (dbsp, extended) Keeping 1 removes the older checkpoint's files
single_host_keeps_one_checkpoint (controller) Catalog has only the newest checkpoint a single host keeps 2
multihost_keeps_two_checkpoints_until_complete (controller) Keeps 2; a missing UUID changes nothing; the right UUID leaves 1 the host ignores the coordinator's message

Also ran the dbsp checkpoint and GC tests, the connector-suite checkpoint, suspend, and restore tests, and the Kafka FT and Postgres CDC tests against local Redpanda and Postgres. test_pipeline_lifecycle.py no longer expects exactly 2 checkpoints.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@blp blp added DBSP core Related to the core DBSP library ft Fault tolerant, distributed, and scale-out implementation storage Persistence for internal state in DBSP operators multihost Related to multihost or distributed pipelines labels Nov 10, 2026
@blp blp self-assigned this Nov 10, 2026
mihaibudiu
mihaibudiu approved these changes Nov 10, 2026
View reviewed changes
Comment thread crates/connectors/src/engine.rs
/// Deletes the checkpoints that are older than `uuid`, except those in
/// `keep`. A host of a multihost pipeline does this when the coordinator
/// says that every host has `uuid`.
fn gc_checkpoints_before(

@mihaibudiu mihaibudiu Nov 10, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

is this using the timestamp encoded in the uuid?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@blp blp Nov 10, 2026

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

No. "Older" means written earlier. The checkpointer keeps checkpoint_list in the order the checkpoints were written: each committed checkpoint goes onto the end (crates/dbsp/src/circuit/checkpointer.rs:437). gc_checkpoints_before finds uuid in that list by position and keeps it and everything after it (checkpointer.rs:580-581). The UUID's timestamp is never read.

If uuid is not in the list, nothing is deleted and a warning is logged, so a stale or wrong UUID cannot delete anything.

-- Claude Code, replying at @blp's request.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@blp
blp enabled auto-merge November 10, 2026 00:57
@blp
blp force-pushed the single-checkpoint branch from 79363f2 to fac88f8 Compare November 10, 2026 00:57
@ryzhyk

ryzhyk commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

I asked claude to check if this PR can somehow cause data loss. It didn't find new bugs, but it uncovered two existing ones, one of which got worse with this PR. Reporting them here, although they may require separate PRs.

The PR's own GC can't delete any file of the newest checkpoint; I traced it end to end. But it shouldn't merge as-is: combined with the cloud S3 pull, it can roll a pipeline back to an older checkpoint on an ordinary restart, and I reproduced that. It also leaves a pre-existing multihost hole open, while the PR description says keeping two checkpoints prevents it.

1. Blocker: start_from_checkpoint: "<uuid>" rolls the pipeline back (reproduced)

1. A pipeline starts from checkpoint X, pulled from the bucket, and writes checkpoint Y.
2. RunningCheckpoint::finish calls Checkpointer::gc_keeping(_, 1), which deletes X locally.
3. On the next start (a crash, stop/start, or suspend/resume), the server calls ControllerBuilder::pull_once, which reaches the cloud's pull::pull_checkpoint.
4. pull::check_local_storage(Uuid(X)) doesn't find X, so the pull downloads X again. pull::write_local_catalog replaces the catalog with [X].
5. PipelineStorage::gc_startup then deletes Y and its batch files, and open_latest_checkpoint resumes from X.

Y is lost without any error. X stays in the bucket for at least 30 days and until 10 newer checkpoints are pushed (the default retention), so every restart in that period rolls back again.

I ran a probe: a real dbsp circuit, a local rclone serve s3 as the bucket, the cloud's real push_checkpoint and pull_checkpoint, and this PR's dbsp. "Keys lost" counts keys that were absent from the restored state.

┌────────────────────────┬─────────────────────┬───────────────────────┬────────────┬────────────┐
│        GC rule         │ Checkpoints after X │ Catalog after restart │ Y survived │ Keys lost  │
├────────────────────────┼─────────────────────┼───────────────────────┼────────────┼────────────┤
│ This PR (keep 1)       │ 1                   │ [X]                   │ no         │ 100 of 100 │
├────────────────────────┼─────────────────────┼───────────────────────┼────────────┼────────────┤
│ Before the PR (keep 2) │ 1                   │ [X, Y]                │ yes        │ 0          │
├────────────────────────┼─────────────────────┼───────────────────────┼────────────┼────────────┤
│ Before the PR (keep 2) │ 2                   │ [X]                   │ no         │ 200 of 200 │
└────────────────────────┴─────────────────────┴───────────────────────┴────────────┴────────────┘

- The root cause predates this PR. It is in the cloud pull, and the old rule fails the same way one checkpoint later.
- What the PR changes: a pipeline that checkpoints only at suspend used to survive its first suspend/resume, and now doesn't. With periodic checkpoints, the safe window shrinks by one interval.
- Fix, before or with this PR: make the pull treat a UUID as a one-time seed. Record the seeded UUID in a marker file, like standby's activation marker. On later starts, use the local catalog if it isn't empty.
- No existing test catches this. test_from_uuid restarts only once, after clearing storage.

2. Multihost: two failed checkpoints in a row lose all state (existed before the PR; inferred from code, not reproduced)

- Hosts trim without coordination: after every local success, a host trims to its 2 newest checkpoints, whatever happened on the other hosts.
- Failures don't stop checkpointing: the coordinator's main loop ignores a failed periodic checkpoint (let _ = self.checkpoint(...)).
- No common checkpoint means a fresh start: Coordinator::choose_checkpoint then returns None, and the pipeline starts fresh with no error.

┌────────────────────────┬───────────────────┬────────────────┬──────────────────────────┐
│ Coordinated checkpoint │ Host A (succeeds) │ Host B (fails) │ Checkpoint on every host │
├────────────────────────┼───────────────────┼────────────────┼──────────────────────────┤
│ after N and complete   │ N                 │ N              │ N                        │
├────────────────────────┼───────────────────┼────────────────┼──────────────────────────┤
│ N+1                    │ N, N+1            │ N              │ N                        │
├────────────────────────┼───────────────────┼────────────────┼──────────────────────────┤
│ N+2                    │ N+1, N+2          │ N              │ none                     │
└────────────────────────┴───────────────────┴────────────────┴──────────────────────────┘

Failures on two different hosts give the same result. I traced both rules through these sequences, and this PR doesn't make it worse. With the new complete message, the fix is cheap: a host keeps everything from the last checkpoint the coordinator confirmed, or from the one it was activated from. It falls back to keeping 2 only if it has never received a confirmation, as with an old coordinator.

3. Low: a sync can name a checkpoint that GC has just deleted (inferred, not reproduced)

- The window: the /checkpoint/sync handler reads the catalog from disk. The UUID is protected only once CircuitThread::run_commands registers it, so a checkpoint finishing in between can delete it.
- The result: the push fails ("failed to read checkpoint directory") and leaves an empty <uuid>.zip in the storage root that is never cleaned up. No data is lost.
- Before the PR, this took two checkpoints finishing in that window; now one is enough. The coordinator's periodic multihost push races with complete in the same way.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp
blp disabled auto-merge November 10, 2026 02:01
@blp
blp added this pull request to the merge queue Nov 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 10, 2026

Uh oh!

There was an error while loading. Please reload this page.

@mythical-fred-oss mythical-fred-oss Bot mentioned this pull request Nov 10, 2026
Flaky: dynamodb_conditional_put_skips_existing_row (1 row instead of 2) #7908
Open
@blp
blp force-pushed the single-checkpoint branch from fac88f8 to 3d13bb7 Compare November 10, 2026 19:35
@blp
blp added this pull request to the merge queue Nov 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 10, 2026

Uh oh!

There was an error while loading. Please reload this page.

blp added 2 commits November 10, 2026 14:34
@blp
[dbsp] Keep only the newest checkpoint on a single host.
33ec6d3
A pipeline kept its two newest checkpoints.  The older one is not
needed: the newest checkpoint lists every batch that the pipeline used
when it wrote it, so the older checkpoint's other files are unused.
Keeping it could use as much storage again as the pipeline's state.

`gc_checkpoint` now takes the number of checkpoints to keep.  A single
host keeps 1.  A host of a multihost pipeline still keeps 2, because a
restart resumes from the newest checkpoint that every host has.

Fixes #7035.

Signed-off-by: Ben Pfaff <[email protected]>
@blp
[adapters] Delete a multihost host's older checkpoint once every host…
63b58ef
… has the newest.

A host of a multihost pipeline keeps its previous checkpoint, because a
restart resumes from the newest checkpoint that every host has.  After
a checkpoint succeeds on every host, the coordinator can now say so with
`POST /coordination/checkpoint/complete`, which names this host's copy
of that checkpoint.  The host then deletes its older checkpoints, with
the new `gc_checkpoints_before`.

The request names the checkpoint, instead of only saying "delete the
older one", so that a late request cannot delete a checkpoint that
another host still lacks.

Signed-off-by: Ben Pfaff <[email protected]>
@blp
blp force-pushed the single-checkpoint branch from 3d13bb7 to 63b58ef Compare November 10, 2026 22:35
@blp
blp enabled auto-merge November 10, 2026 22:35
@blp
blp disabled auto-merge November 10, 2026 22:52
@blp
blp added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit e223878 Nov 11, 2026
1 check passed

Uh oh!

There was an error while loading. Please reload this page.

@blp
blp deleted the single-checkpoint branch November 11, 2026 03:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@mihaibudiu mihaibudiu mihaibudiu approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@blp blp

Labels

DBSP core Related to the core DBSP library ft Fault tolerant, distributed, and scale-out implementation multihost Related to multihost or distributed pipelines storage Persistence for internal state in DBSP operators

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

[dbsp] Keeping two checkpoints around is wasteful

Uh oh!

There was an error while loading. Please reload this page.

3 participants

@blp @ryzhyk @mihaibudiu
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.