Skip to content

feat: Persist source state only after destinations commit the synced records - #23546

Draft
disq wants to merge 12 commits into
mainfrom
feat/cli-checkpoints
Draft

disq wants to merge 12 commits into
mainfrom
feat/cli-checkpoints

Conversation

@disq

@disq disq commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

When every destination supports flush, the CLI flushes them before it writes each source state checkpoint to the backend.

Tested with a local SQLite sync: after a source crash, the stored cursor never passed the committed rows.
Depends on cloudquery/plugin-pb#42 and cloudquery/plugin-pb-go#710: go.mod replaces plugin-pb-go with that branch until it is released.
Checkpoints stay off with transformers or destinations without flush support.
If a destination or the state backend reports a failed flush, the CLI saves no more state for that source, finishes the sync and exits with an error. Other sources still sync.
A destination that ends its stream after it reports the failure still lets the sync finish.
Side effects of this policy:

  • A row that a destination always rejects stops every incremental cursor of the source, not only the cursor of its table.
  • Delete-stale is skipped for the failed tables on every such run, or for every table if the destination does not name them.
  • In append mode, each rerun appends again the rows synced after the last saved state.

Also fixes delete-stale running only on the first overwrite-delete-stale destination: UpdateTableNames removed the tables from its input map.

@erezrokah

Copy link
Copy Markdown
Member

Can we flush pending checkpoints while the source is idle and split large checkpoint batches to stay within the gRPC message limit?

@disq

disq commented Oct 14, 2026 •

Copy link
Copy Markdown
Member Author

@erezrokah Both done in 118eac4:

  • The CLI now receives sync messages on a separate goroutine, so pending state is persisted when the 30s interval elapses even while the source sends nothing (TestCheckpointerCommitsWhileSourceIsIdle).
  • State written to the backend is split into records of about 64 MiB, well under the 200 MiB gRPC limit (TestCheckpointerSplitsLargeState).

Checkpoint messages from the source are not split yet: a single Flush larger than the SDK's 100 MiB message limit still fails the sync. I can split those in cloudquery/plugin-sdk#2632 if you want.

disq added 2 commits October 14, 2026 13:01
- Validate checkpoint column types instead of panicking on an unexpected version type
- Report a pipeline's error instead of a cancelled commit when a pipeline fails during a commit
- Name the destination whose flush failed
- Log when the source sends checkpoints, instead of claiming they are enabled up front
- Test the checkpoint flow through syncConnectionV3 with fake plugins, and the switch that enables it
@erezrokah

Copy link
Copy Markdown
Member

Could we apply checkpointCommitError to the receive-error path too? When a destination write fails while the source is idle, the receiver can report only context canceled, hiding the actual destination error.

@disq

disq commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

@erezrokah Done in 30ee29e: the receive-error path now goes through the same check (renamed to unlessFailedElsewhere), so when a pipeline's destination write fails while the source is idle, the sync returns the destination error instead of context canceled. TestSyncReportsDestinationErrorWhileSourceIsIdle covers it end to end.

disq added 5 commits October 14, 2026 19:19
- Build and send state records one at a time, and count every column in the record size estimate
- Report a duplicate state column as such, and route receive errors through receiveError
- Test that a flush waits for pipeline delivery, idle commits through syncConnectionV3, backend errors across several records, and prefix matching of the state backend
- Flush every destination that has not failed yet at the end of the sync, so later failures skip delete-stale too
- Keep syncing other sources when a sync finishes without saving its state, and report every such source at the end
- Treat a failed state backend flush like a failed destination flush
- Report destination commit failures together with hard flush errors
- Prefer the pipelines' errors over the receive loop's instead of filtering each loop error
- Word the error differently for sources that sent no checkpoints
- Warn about skipped delete-stale only for destinations that run it
- Bump plugin-pb-go to the regenerated flush_error wording
- Skip delete-stale only for the tables a destination failed to write, when it names them
- Ignore the end of a write stream after the destination reported a failed flush, and close every destination before returning
- Keep the unsaved state errors of earlier sources when a later source fails
- Pin the preference of pipeline errors over the receive loop's in a helper with a deterministic test
- Warn append mode destinations that the next sync appends the unsaved rows again
- Stop UpdateTableNames from removing tables from its input, which skipped delete-stale on every destination after the first
- Bump plugin-pb-go to the failed_tables field

This branch has not been deployed

No deployments
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.

2 participants