Skip to content

feat: Send incremental state as checkpoints after the resources it covers - #2632

Draft
disq wants to merge 15 commits into
mainfrom
claude/incremental-syncs-design-08b5fc
Draft

disq wants to merge 15 commits into
mainfrom
claude/incremental-syncs-design-08b5fc

Conversation

@disq

@disq disq commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds source checkpoints and destination flushes, so the CLI can persist state only after its resources commit.

Sources opt in with state.ClientOptions{Checkpoints: res}, destinations with plugin.WithFlushSupport().
Behaviour change: with checkpoints on, a failed batch write is reported on the next flush, with the tables it failed for, so the CLI stops saving state for that sync instead of only logging the failure.
After a cancelled sync, including a quota stop, Flush returns state.ErrSyncAborted for keys set in table resolvers and does not save them, as the scheduler may have dropped their rows.
scheduler.Sync now returns the cancel cause, not nil, when the context is cancelled while the batcher closes.
Also fixes NewConnectedClientWithOptions with a zero MaxMsgSizeInBytes, which set a 0-byte gRPC message limit.
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.

@erezrokah

Copy link
Copy Markdown
Member

Found two issues in the streaming flush path:

  • Flush can report success after startWorker rejects an insert, allowing a checkpoint to advance past unwritten rows. Dispatch errors should propagate to the flush result.
  • Cancellation during commit abandons the unbuffered acknowledgement channel, leaving the worker and Close blocked indefinitely. A buffered or cancellation-aware acknowledgement would avoid this.

Both reproduced in focused tests.

@disq

disq commented Oct 14, 2026 •

Copy link
Copy Markdown
Member Author

@erezrokah Thanks, both reproduced and fixed in ca3497e:

  • A message startWorker rejects now fails every later flush in that Write call, so a checkpoint cannot pass unwritten rows (TestFlushMessageReportsRejectedMessages).
  • The commit acknowledgement channel is buffered, so a cancelled flush no longer blocks the worker or Close (TestFlushMessageCancelledDuringCommit).

disq added 2 commits October 14, 2026 12:50
- Skip a checkpoint when the sync is cancelled while flushing the batcher
- Make a key changed by several resolvers wait for all of their resources
- Try every checkpoint group in Flush, unscoped keys first, and export ErrSyncAborted
- Coalesce state held until a shuffle-queue sync completes
- Let clients turn flush support off per spec, and document that a flush ends open batches
- Only honour ClientOptions.Checkpoints in NewConnectedClientWithOptions
- Stop WriteStream when the plugin stops writing, and reject empty write messages
- Test that a flush is acknowledged only after it commits, and only if it succeeds
@erezrokah

Copy link
Copy Markdown
Member

The original issues are fixed. I reproduced two remaining problems:

  • A later flush of a shared cursor can overtake an earlier resolver’s unfinished rows because flush clears its pending dependencies.
  • Shuffle-queue can combine valid checkpoints into one oversized message, causing the sync to fail.

Could we preserve shared-key dependencies across flushes and split coalesced checkpoints to respect the message limit?

- A checkpoint of a key waits for the previous checkpoint of that key to be sent, so a later flush cannot overtake rows an earlier resolver has not sent yet
- Checkpoints merged during a shuffle-queue sync are marked sent once the merged checkpoint is sent
- Oversized checkpoints are split across several messages, like inserts
@disq

disq commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

@erezrokah Both fixed in 363038e:

  • The state client keeps each key's last checkpoint until it is sent, and the next checkpoint of that key waits for it, so a later flush of a shared cursor can no longer overtake an earlier resolver's unsent rows (TestStateClientCheckpointWaitsForPreviousCheckpointOfKey). Checkpoints merged during a shuffle-queue sync count as sent once the merged one is sent.
  • The server now splits an oversized checkpoint across several messages, the same way it splits inserts, so a merged shuffle-queue checkpoint or a single large Flush stays within the message limit (TestSendRecordSplitsOversizedCheckpoint).

@erezrokah

Copy link
Copy Markdown
Member

All four earlier issues are fixed. I found one regression: dependency barriers bypass shuffle-queue coalescing, retaining every intermediate cursor record until sync completion.

The same state-client test produces 1 checkpoint for 100 flushes on the previous revision, but 100 checkpoints now. Could we preserve coalescing when tracking prior-checkpoint dependencies and cover the actual SetKey/Flush path in a regression test?

@disq

disq commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

@erezrokah Fixed in b2202a5: a scope can now say a checkpoint passed to it is never sent before one it already holds, and the shuffle-queue scope does so for checkpoints merged into its held state. A repeated flush of a key in a shuffle-queue sync then skips the dependency barrier and is merged again. TestShuffleQueueCoalescesFlushedCheckpoints covers the SetKey/Flush path: 100 flushes produce one checkpoint, sent after every row.

disq added 6 commits October 14, 2026 19:09
- Track the scopes of a key only with checkpoints, as a set, and collapse the scopes of ended table resolvers into one, so SetKey stays O(1) and Flush linear
- A same-value SetKey of a pending key also waits for the caller's resources
- Abort checkpoints the scheduler drops, and the checkpoints waiting for them, so a later Flush of their keys returns ErrSyncAborted and keeps them pending
- Route a checkpoint changed outside resolvers through the scope holding what it waits for, so shuffle-queue keeps merging
- Do not report flush support for clients without a connection
- Name the table in batch writer flush errors, and document the failed batch policy on WithFlushSupport
- Cover deletes before a flush, SkipInsert and stuck flushes in the conformance test, and the WriteStream exits when a plugin stops writing
- Guard flush errors against empty and invalid UTF-8 messages
- Abort checkpoints queued behind resolvers that are stuck after a cancel
- Drop checkpoints sent after SyncAll returns instead of panicking
- Send keys changed by the same scopes in one checkpoint
- Document that Write wrappers must forward every WriteFlush
- Bump plugin-pb-go to the regenerated flush_error wording
- Cover the stuck resolver, scope collapse, writer flush failures and WriteStream ending after a failed flush
- Name the tables of a failed flush, and report each write stream only the failures of its own tables
- Return the cancel cause from Sync when it drops checkpoints after the batcher closes
- Compact scope sets again only once they double, and lock the root of merged checkpoints reliably
- Describe flush results as attempts to commit, and the append mode duplicates of a failed flush
- Test the order of checkpoints from ended resolvers, scope compaction, keys with different previous checkpoints, stuck resolvers with a state client, the send guard and the flush wait
- Bump plugin-pb-go to the failed_tables field
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants