Repository navigation
feat(declarative): add SyncTokenCursor for server-issued sync tokens - #1234
Conversation
Co-Authored-By: bot_apk <apk@cognition.ai>
Co-Authored-By: bot_apk <apk@cognition.ai>
Co-Authored-By: bot_apk <apk@cognition.ai>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
👋 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@devin/1792189821-opaque-token-cursor#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 devin/1792189821-opaque-token-cursorPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
PyTest Results (Full)6 078 tests 6 047 ✅ 9m 47s ⏱️ Results for commit e954929. ♻️ This comment has been updated with latest results. |
Co-Authored-By: bot_apk <apk@cognition.ai>
This comment was marked as resolved.
This comment was marked as resolved.
|
👍 On it. I'm applying findings 1–12 on this branch as new commits on top of a merge of
No change for 13. After that I'll merge the updated branch into #1235. For finding 4, I'll change |
Co-Authored-By: bot_apk <apk@cognition.ai>
Co-Authored-By: bot_apk <apk@cognition.ai>
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
…tition routers Co-Authored-By: bot_apk <apk@cognition.ai>
This comment was marked as resolved.
This comment was marked as resolved.
…set' into devin/1792471568-sync-token-request-path Bring in the current base (#1235, which includes #1234's review fixes) and resolve conflicts: - model_to_component_factory.py: keep both new static methods, _validate_sync_token_option_collisions (this PR) and _flatten_partition_router_models (base). - sync_token_request_options_provider.py: keep this PR's RequestPath support and _get_token helper; take the base's docstring, its removal of the dead `not self.token_option` check, and valid_types=(str,) for initial_token. - sync_token_cursor.py: say "sync token" in the log line of this PR's new first-commit branch, matching the base's rename. test_commit_observability_counts_completed_pages_per_partition fails otherwise.
Review follow-up: malformed saved state starts a silent full readReviewed at P1: malformed
|
Co-Authored-By: bot_apk <apk@cognition.ai>
|
Re: review follow-up on malformed saved state ☑️ Resolved in 4050495. Malformed
Posted by Devin, as requested by Patrick Nilan (@pnilan). |
…partition state - Remove the hand-added interpolation_context from the generated SyncTokenCursor model. The generator only emits the deprecated and deprecation_message extra keys. - Add a read test for a token stream used as a parent without incremental_dependency: the parent copy is read in full, sends no stored token and emits no state. - Cover more malformed partitioned state shapes and assert system_error. - Drop two tests whose names said malformed shapes are ignored. The malformed-state tests cover the same shapes, expect the failure and check that nothing is loaded.
Baz (bazarnov)
left a comment
There was a problem hiding this comment.
Approving, with one condition under Before merge. SyncTokenCursor is opt-in and inert for every existing stream, and I found nothing else blocking.
Verified
- Fleet: built and discovered all 540 declarative manifests from
airbytehq/airbytemaster (5,448 streams, with and without incoming state) onmainand on this branch. Catalogs, errors and warnings are identical. - Runtime: a token commits only after its partition's records (stress runs with failing and vanishing partitions and a bounded queue). Malformed and foreign state behave as described end to end, and the token reaches requests through all four
inject_intotargets, including on partitioned streams. - source-ashby without #1235: with its
RESET_SYNC_TOKENfilter removed, the #89675 manifest builds on this branch. Mocked reads cover a cold read, a warm read, an expired token (the stream fails and keeps its stored token) and theinterview_plansparent. - Platform contract:
source_defined_cursor: truewithdefault_cursor_field: []is accepted for Incremental Append and Append + Deduped (cursor validation, default sync modes). Bulk CDK destinations order byemittedAtwhen the cursor is empty (DestinationStream.kt). - CI on e954929: all checks pass except
Check: destination-motherduck, which fails at secret fetch (SECRET_DESTINATION_MOTHERDUCK__MOTHERDUCK__CREDShas no enabled version), as on other CDK PRs. It is unrelated to this change. - Breaking change: none. The schema only gains an
anyOfbranch. Every behavior change in shared code is gated onSyncTokenCursor(page_observerstaysNone,source_defined_incrementalstaysFalse), and the other shared edits are refactors with no behavior change, which the fleet run confirms.
Pushed in e954929, on top of a main merge
- Removed the hand-added
interpolation_contextfrom the generatedSyncTokenCursormodel. The generator only emitsdeprecatedanddeprecation_message(generator). - Added a read test for a token stream used as a parent without
incremental_dependency, the shape source-ashby uses forinterview_stages(#89675), and more malformed partitioned-state cases. Each new case fails on a mutant of the line it guards. - Dropped two tests whose names said malformed shapes are ignored. The malformed-state tests cover the same shapes, expect the failure and check that nothing is loaded.
- Updated the description for the standalone release.
Before merge
- Patrick Nilan (@pnilan), the merge gate is checked, but the only live-validation record I found (#89675) says a warm read that returns a changed record was not produced. Please link the mid-pass edit result on airbytehq/airbyte-internal-issues#17213. If edits made during a pass are missed, #17213 calls for the overlap option before release.
Notes for follow-ups (aside), no changes needed in this PR
- With #1235 deferred, a rejected token is not recovered automatically, and Ashby tokens are valid for 14 days (Ashby docs). The Ashby adoption has to drop
RESET_SYNC_TOKEN, which this schema does not accept (#89675), along with the docs and AGENTS.md lines that promise a full re-read on these errors. The existingsuccess_falsefilter then fails the stream with asystem_errorand no refresh instruction (manifest). A dedicatedFAILfilter withfailure_type: config_errorforsync_token_expired,sync_token_invalidandincremental_sync_too_largethat asks for a refresh would be clearer. NeverIGNOREthem: the stored token is kept, so every later sync reads nothing. - #1238 says its new checks apply to an unreleased cursor. Once this ships, they tighten a published contract and need a breaking-change look.
incremental-syncs.mddocuments onlyDatetimeBasedCursorso far (the docs step in #17213).
|
/prerelease
|
Confirmed working. Can't inject state into regression tests so Devin ran a test read with output cursors from previous regression test run. |
…eset Co-Authored-By: bot_apk <apk@cognition.ai> # Conflicts: # unit_tests/sources/declarative/incremental/test_sync_token_cursor.py
Overview
👉 TL;DR: Lets low-code connectors sync incrementally from APIs that hand back a "sync token" at the end of a read, instead of filtering by a timestamp. The token is saved only after every record from that read has been emitted, and is sent back on the next sync to fetch only what changed.
Specifically, this adds the
SyncTokenCursorincremental type,SyncTokenRequestOptionsProvider, and apage_observerhook (PageCompleted) onSimpleRetriever.This PR ships on its own for source-ashby, once the source-ashby adoption (airbytehq/airbyte#89675) drops its
RESET_SYNC_TOKENfilter. The follow-ups are in draft: #1235 (RESET_SYNC_TOKENrecovery) and #1238 (RequestPathtokens and first-request checks).Part of https://github.com/airbytehq/airbyte-internal-issues/issues/17213. Close #17213 only after the whole stack ships and its acceptance criteria are checked off.
Merge gate
Keep this PR in draft until all of these are done:
Changes
SyncTokenCursordefinition withtoken_path,token_option,completion_condition,initial_tokenandcursor_field, added toDeclarativeStream.incremental_sync.anyOf.SyncTokenCursor(incremental/sync_token_cursor.py):close_partition, after the pass's records.{"state_type": "sync_token", "token": "..."}, or astateslist per partition, capped at 25,000 partitions.sync_tokenstate fails the stream with asystem_errorthat asks for a refresh, and is left in place.SyncTokenRequestOptionsProvider: sends the stored token, elseinitial_token, else nothing, throughtoken_option. Wrapped inPerPartitionRequestOptionsProviderfor partitioned streams.SimpleRetriever: newpage_observerhook, called withPageCompleted(stream_slice, response, pagination_exhausted)after each page. Bound only forSyncTokenCursor;Noneeverywhere else.DefaultStream: newsource_defined_incrementalflag, so token streams advertiseincrementalwith a source-defined cursor.ConcurrentPerPartitionCursorgains a publicpartition_routerproperty used by the same lookup.ModelToComponentFactory:UnionPartitionRouterandGroupingPartitionRouterat any depthapi_retention_periodExample (Ashby):
Recommended review order
Tests are left out. This PR doesn't change the entrypoint or the source (
ConcurrentDeclarativeSource); the new type is reached through the schema and the factory.declarative_component_schema.yaml#L1431-L1493: theSyncTokenCursordefinitiondeclarative_component_schema.yaml#L1911: added toincremental_sync.anyOfmodels/declarative_component_schema.py#L2976: generated modelparsers/model_to_component_factory.py)#L2185-L2200and#L2318: request options provider andsource_defined_incrementalincreate_default_stream#L2322-L2463:_flatten_partition_router_modelsand_validate_sync_token_stream#L2584-L2592and#L2644-L2688:_build_concurrent_cursorbranch andcreate_sync_token_cursor#L4235-L4237:page_observerbinding increate_simple_retriever#L4852-L4857:api_retention_periodrejectionincremental/sync_token_cursor.py: start aton_page_completed, thenclose_partitionand_load_staterequesters/request_options/sync_token_request_options_provider.py: token injectionretrievers/simple_retriever.py#L84-L90and#L562-L569:PageCompletedand the hook call sitestreams/concurrent/default_stream.py#L89-L93and#L127: discovery flags and partition-router lookupincremental/concurrent_partition_cursor.py#L201-L203: newpartition_routerpropertyincremental/__init__.py,requesters/request_options/__init__.py: exportsBreaking change
None. The component is additive and opt-in. When it isn't used,
page_observerisNoneandsource_defined_incrementalisFalse, so existing cursors, streams and the retriever behave as before.Risks
FAILwith a refresh instruction, never toIGNORE: an ignored error without a new token keeps the stored token, so every later sync reads nothing, and an ignored 2xx response with a token attoken_pathcommits that token.Test plan
unit_tests/sources/declarative/incremental/test_sync_token_cursor*.py: cursor state rules, schema, discovery and build-time validation, nested partition routers, plus end-to-end reads throughConcurrentDeclarativeSource.readwithHttpMockerthat assert exact request bodies and emitted state.main(schema validation error) and pass with this change.poetry run pytest unit_tests/sources/declarative/incremental/ unit_tests/sources/declarative/parsers/test_model_to_component_factory.py: 509 passed. Ruff and MyPy pass.Design notes: why a new cursor type
Show/Hide Content
DatetimeBasedCursorandIncrementingCountCursorboth derive state from record values. No existing declarative component can capture a value from the response envelope and commit it only when the pass completes.SyncTokenCursoris built in and declarative, so adopting connectors need no custom Python components.Link to Devin session: https://app.devin.ai/sessions/94b41efe75db43e78eb5a4f8d5f58484
Open in Devin Desktop: https://app.devin.ai/desktop/session/94b41efe75db43e78eb5a4f8d5f58484?variant=devin