Repository navigation
fix(low-code): don't emit a progress-free state when a global-cursor stream fails - #1254
Conversation
…ream fails
ensure_at_least_one_state_emitted ran on failed streams too. A stream that
failed before checkpointing anything emitted {"use_global_cursor": ...,
"lookback_window": ...}, which the next attempt reads as "already synced
once": a StateDelegatingStream then takes its incremental stream and never
reads the slices the failed attempt missed.
Skip the emit when a partition was never closed or slice generation stopped
early and the state has no global cursor, per-partition states or parent
state. Completed streams, including ones with zero records, still emit.
👋 Greetings, Airbyte Team Member!Here are some helpful tips and reminders for your convenience. 💡 Show Tips and TricksTesting This CDK VersionYou can test this version of the CDK using the following: # Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@tolik0/skip-empty-state-after-failed-partition#egg=airbyte-python-cdk[dev]' --help
# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch tolik0/skip-empty-state-after-failed-partitionPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
PyTest Results (Fast)5 954 tests +52 5 923 ✅ +52 5m 43s ⏱️ -38s Results for commit 7862272. ± Comparison against base commit 3e85723. This pull request removes 1 and adds 53 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
PyTest Results (Full)5 957 tests +52 5 926 ✅ +52 10m 13s ⏱️ -50s Results for commit 7862272. ± Comparison against base commit 3e85723. This pull request removes 1 and adds 53 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
ZaneHyattAB
left a comment
There was a problem hiding this comment.
This looks good. I tested it with the #89581 manifest. After a failed sync, the retry now does the full read instead of skipping.
One question: connections that already saved {"use_global_cursor": true, "lookback_window": 0} from an earlier failure will still skip the full read after this fix. Should we treat that state as empty here, or handle those connections another way?
Daryna Ishchenko (darynaishchenko)
left a comment
There was a problem hiding this comment.
The fix is correct for the case it targets: a global-cursor stream that fails before checkpointing no longer emits {use_global_cursor, lookback_window}. I confirmed that end-to-end against main for both failure modes, child partition failure and router/parent failure. The change is non-breaking, and successful syncs (including zero-record ones) still emit state.
Two inline comments cover sibling cases that still produce a non-empty state without full progress.
Rollout note: for global-cursor streams with no parent state, the final state was the only commit point. Records a failed attempt read are now no longer followed by a state, so destinations won't commit them and the retry reads them again. That's the intended trade, but it's worth mentioning in the release notes.
…fails If the partition router raises after some partitions were read and closed, ensure_at_least_one_state_emitted saw every generated partition closed and advanced the global cursor to the newest record. Partitions that were never generated would then start from that cursor on the next sync. Only advance the cursor once slice generation finished, and scope the docstring to global-cursor streams.
|
ZaneHyattAB thanks for testing it. I'd rather not special-case that state in the factory. A successful sync that reads zero records emits exactly |
…7.40.0 CDK 7.40.0 includes airbytehq/airbyte-python-cdk#1254, so a failed bank_accounts attempt no longer saves a progress-free state. The 400 test now checks that no state is emitted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What
For global-cursor streams,
ConcurrentPerPartitionCursor.ensure_at_least_one_state_emittedchanges in two ways:It no longer advances the global cursor when the stream did not complete. Before, a partition router that raised after some partitions were read and closed left every generated partition closed, so the cursor moved to the newest record, and partitions that were never generated started from it on the next sync.
It no longer emits a state when the stream did not complete and the state holds no checkpoint.
"Did not complete" means a partition was never closed (its read raised) or slice generation stopped early (the partition router raised).
"No checkpoint" means the state has no global
state, no per-partitionstatesand noparent_state. Such a state is only{"use_global_cursor": ..., "lookback_window": ...}.Every other path is unchanged: completed streams still emit (including a stream that read zero records), and failed streams that hold a checkpoint still emit it.
Why
ensure_at_least_one_state_emittedruns when a stream finishes, including when it failed. For a stream that failed before checkpointing anything, it emitted{"use_global_cursor": true, "lookback_window": 0}. The platform persists that state, and the next attempt then starts with a non-empty state that records no progress.For a
StateDelegatingStream, a non-empty state selectsincremental_stream. Its cursor finds no value and starts fromstart_date, so the slices the failed attempt never read are skipped for good, and the retry ends green. The full-refresh walk is never resumed.This hits airbytehq/airbyte#89581 (source-stripe
bank_accounts, a global cursor over a staticListPartitionRouter), and also the connector version it replaces (aSubstreamPartitionRouterwithoutincremental_dependency, whoseparent_stateis always{}).Reproduction with the source-stripe manifest from airbytehq/airbyte#89581
Three 30-day slices, each with one customer holding one bank account. Attempt 1 fails on slice 0, then attempt 2 starts from the state attempt 1 emitted.
customers400{use_global_cursor: true, lookback_window: 0}customers5xx, retries exhaustedTests
test_concurrent_perpartitioncursor.py:mainand pass with this change.unit_tests/sources/declarative,unit_tests/connector_builderandunit_tests/sources/concurrent_sourcesuites pass locally.bank_accountstests from fix(source-stripe): read bank_accounts full refresh without per-customer partitions airbyte#89581 pass with this change (21).Risks
global_substream_cursor: false) is out of scope. Every generated partition's cursor reports its start value, so a failed stream still emits a state. More generally, any partial checkpoint (the first closed partition in per-partition mode, orparent_statein global mode) switches aStateDelegatingStreamto its incremental stream; fixing that needs a "full refresh completed" marker inStateDelegatingStream.🤖 Generated with Claude Code