Repository navigation
feat(low-code): add RateLimitedMultipleTokenAuthenticator component - #1066
Conversation
… component Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
👋 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/1783347505-rate-limited-multiple-token-authenticator#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/1783347505-rate-limited-multiple-token-authenticatorPR Slash CommandsAirbyte Maintainers can execute the following slash commands on your PR:
|
Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
PyTest Results (Full)4 184 tests 4 172 ✅ 12m 7s ⏱️ Results for commit 89d6750. ♻️ This comment has been updated with latest results. |
…on wait Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
… already converted Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a declarative authenticator that rotates multiple tokens using quota pools, refreshes quota state, waits for resets, and applies proactive throttling. Schemas, factory construction and caching, public exports, serialization, and tests are updated. ChangesRate-limited multi-token authentication
Advanced authentication spec serialization
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Request as PreparedRequest
participant Auth as RateLimitedMultipleTokenAuthenticator
participant Quota as quota-status endpoint
Request->>Auth: authenticate request
Auth->>Quota: fetch quota status for tokens
Quota-->>Auth: return remaining and reset values
Auth->>Auth: match quota pool and reserve token
Auth-->>Request: attach authorization header
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
airbyte_cdk/sources/declarative/spec/spec.py (1)
58-69: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy liftCould we normalize a serialized copy instead of mutating the typed model?
generate_spec()assigns.valuestrings back into Enum-backed fields, so after the first callself.advanced_authno longer conforms to its declared model contract. This fixes repeated serialization, but can break callers that reuseadvanced_authand expectAuthFlowType/ScopesJoinStrategyinstances. Could you build the output dictionary first and normalize that copy instead, wdyt?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@airbyte_cdk/sources/declarative/spec/spec.py` around lines 58 - 69, The generate_spec() serialization flow currently mutates Enum-backed fields on self.advanced_auth and its nested oauth configuration. Build or obtain a serialized output copy first, then normalize auth_flow_type and scopes_join_strategy on that copy only, preserving the typed AuthFlow and OAuth connector input models for repeated callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@airbyte_cdk/sources/declarative/auth/rate_limited_multiple_token.py`:
- Around line 165-205: The _acquire_call exhaustion path currently applies
_max_wait_time to each sleep rather than the full retry loop. Track a single
exhaustion deadline across repeated reset/refresh iterations, bound each wait by
the remaining time, and raise the existing transient AirbyteTracedException when
that cumulative budget is exhausted; add a regression test covering repeated
zero-quota refreshes.
In `@airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py`:
- Around line 725-726: The shared authenticator registry must be reused by
nested ModelToComponentFactory instances so parent and child streams share
rate-limit counters. Update the parent-stream factory creation paths around
ModelToComponentFactory and the related factory construction at 4514-4526 to
accept and propagate the parent’s _rate_limited_authenticators mapping, rather
than initializing a fresh registry; preserve the existing local initialization
only for root factories.
In `@unit_tests/sources/declarative/auth/test_rate_limited_multiple_token.py`:
- Around line 186-211: Update
test_thread_safety_header_token_matches_decremented_token to capture exceptions
raised by each worker in make_call, using the existing thread coordination or an
equivalent mechanism, and assert that the collected exception list is empty
after all threads join. Keep the signed token collection and per-token
remaining-quota assertions unchanged.
---
Nitpick comments:
In `@airbyte_cdk/sources/declarative/spec/spec.py`:
- Around line 58-69: The generate_spec() serialization flow currently mutates
Enum-backed fields on self.advanced_auth and its nested oauth configuration.
Build or obtain a serialized output copy first, then normalize auth_flow_type
and scopes_join_strategy on that copy only, preserving the typed AuthFlow and
OAuth connector input models for repeated callers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 53a02d6d-190c-4424-9f9f-d4b367840869
📒 Files selected for processing (7)
airbyte_cdk/sources/declarative/auth/__init__.pyairbyte_cdk/sources/declarative/auth/rate_limited_multiple_token.pyairbyte_cdk/sources/declarative/declarative_component_schema.yamlairbyte_cdk/sources/declarative/models/declarative_component_schema.pyairbyte_cdk/sources/declarative/parsers/model_to_component_factory.pyairbyte_cdk/sources/declarative/spec/spec.pyunit_tests/sources/declarative/auth/test_rate_limited_multiple_token.py
…es; capture worker exceptions in thread-safety test Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
Anatolii Yatsuk (tolik0)
left a comment
There was a problem hiding this comment.
Deep review with local reproductions (ran the PR's tests, drove it end-to-end through ConcurrentDeclarativeSource manifests, replicated the model codegen, and compared behavior against source-github's MultipleTokenAuthenticatorWithRateLimiter). The core machinery works and thread safety is done correctly, but two must-fix items remain — see inline comments:
- High: the shared-instance cache key is poisoned by
$parameterspropagation, so streams get separate counter sets in realistic manifests (reproduced on this head). - Medium: the checked-in generated models file is out of sync with the schema (hand-edited, not regenerated).
Also a medium on max_wait_time not being a cumulative bound (reproduced infinite reseed loop), plus several low-severity robustness notes. The substream-factory registry sharing added in d92aaac was verified working with a parent/child manifest.
…enticator - Build shared-instance cache key from resolved constructor args so propagated $parameters cannot break instance sharing - Enforce cumulative max_wait_time across an exhaustion episode to prevent infinite reseed loops - Regenerate declarative_component_schema.py via codegen - Filter empty tokens in list form; validate max_wait_time is a fixed-length duration - Warn once on unmatched quota fallback; raise config_error for empty quotas - Share authenticator registry via constructor injection - Make Spec.generate_spec idempotent (normalize serialized dict, never mutate the model) Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
…tible style Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
…antics - Add minItems: 1 to `quotas` and to the array branch of `tokens` so an empty list is rejected at manifest validation time (e.g. in the Builder UI) instead of at stream construction. datamodel-codegen maps the quotas constraint to min_items=1 on the pydantic field; the tokens constraint is enforced at the JSON-schema layer only (pydantic v1 cannot constrain a single Union member), with the existing runtime guard still covering the string-splitting path. - Add a comment to _seed_all_tokens explaining why the wholesale state swap intentionally discards local decrements made during the fetch window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (3)
unit_tests/sources/declarative/spec/test_spec.py (1)
165-180: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winCover nested
scopes_join_strategypreservation.This test does not execute the nested OAuth scope conversion. Could you add an OAuth connector input specification with a
scopes_join_strategyenum, then assert that the generated value is serialized and the source model remains unchanged after both calls? wdyt?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@unit_tests/sources/declarative/spec/test_spec.py` around lines 165 - 180, Extend test_generate_spec_is_idempotent_and_does_not_mutate_the_model to include an OAuth connector input specification containing a scopes_join_strategy enum. After both generate_spec calls, assert the generated specifications serialize and preserve that nested strategy, while also verifying the source model’s scopes_join_strategy remains unchanged and the two generated results are identical.airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py (1)
4585-4589: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueShould this be a config error rather than a bare
ValueError?A calendar-unit
max_wait_timeis a manifest/config mistake. The authenticator itself reports comparable problems withAirbyteTracedException(failure_type=FailureType.config_error, ...), which yields a user-facing message instead of a system error trace. Would raising the same exception type keep the failure classification consistent, wdyt?♻️ Proposed change
if not isinstance(max_wait_time, datetime.timedelta): - raise ValueError( - f"max_wait_time must be a fixed-length ISO 8601 duration (e.g. 'PT2H'); " - f"calendar-unit durations like '{max_wait_time_str}' are not supported" - ) + raise AirbyteTracedException( + failure_type=FailureType.config_error, + internal_message=( + f"max_wait_time must be a fixed-length ISO 8601 duration (e.g. 'PT2H'); " + f"calendar-unit durations like '{max_wait_time_str}' are not supported" + ), + message="Invalid maximum wait time: use a fixed-length duration such as 'PT2H'.", + )Note:
test_factory_rejects_calendar_unit_max_wait_timeassertspytest.raises(ValueError, match="calendar-unit"), so the test would need the new type and a match against the internal message.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py` around lines 4585 - 4589, Update the calendar-unit validation in the max_wait_time parsing path to raise AirbyteTracedException with FailureType.config_error instead of ValueError, preserving the existing descriptive message. Adjust test_factory_rejects_calendar_unit_max_wait_time to expect the new exception type and match the internal error message while retaining coverage of the calendar-unit rejection.unit_tests/sources/declarative/auth/test_rate_limited_multiple_token.py (1)
273-276: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCould the clock patches target the authenticator module instead of the global
timemodule?
patch("time.monotonic", ...)replaces the attribute on the sharedtimemodule, so every library in the process sees the fake clock for the duration of the test.requests_mockintercepts the HTTP call, so this currently works, but a module-scoped patch is more isolated. Would patchingairbyte_cdk.sources.declarative.auth.rate_limited_multiple_token.time.monotonic/.sleep(or usingmonkeypatch.setattron that module) read more precisely here, wdyt?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@unit_tests/sources/declarative/auth/test_rate_limited_multiple_token.py` around lines 273 - 276, Update the clock patches in the test context around the rate-limited authenticator to target the authenticator module’s imported time object, using its rate_limited_multiple_token.time.monotonic and time.sleep attributes instead of the global time module. Keep the existing fake_sleep and clock["now"] behavior unchanged while limiting the patch scope to the code under test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py`:
- Around line 4585-4589: Update the calendar-unit validation in the
max_wait_time parsing path to raise AirbyteTracedException with
FailureType.config_error instead of ValueError, preserving the existing
descriptive message. Adjust test_factory_rejects_calendar_unit_max_wait_time to
expect the new exception type and match the internal error message while
retaining coverage of the calendar-unit rejection.
In `@unit_tests/sources/declarative/auth/test_rate_limited_multiple_token.py`:
- Around line 273-276: Update the clock patches in the test context around the
rate-limited authenticator to target the authenticator module’s imported time
object, using its rate_limited_multiple_token.time.monotonic and time.sleep
attributes instead of the global time module. Keep the existing fake_sleep and
clock["now"] behavior unchanged while limiting the patch scope to the code under
test.
In `@unit_tests/sources/declarative/spec/test_spec.py`:
- Around line 165-180: Extend
test_generate_spec_is_idempotent_and_does_not_mutate_the_model to include an
OAuth connector input specification containing a scopes_join_strategy enum.
After both generate_spec calls, assert the generated specifications serialize
and preserve that nested strategy, while also verifying the source model’s
scopes_join_strategy remains unchanged and the two generated results are
identical.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 1af9027d-f2f9-45cb-a745-af3b97a80f0d
📒 Files selected for processing (7)
airbyte_cdk/sources/declarative/auth/rate_limited_multiple_token.pyairbyte_cdk/sources/declarative/declarative_component_schema.yamlairbyte_cdk/sources/declarative/models/declarative_component_schema.pyairbyte_cdk/sources/declarative/parsers/model_to_component_factory.pyairbyte_cdk/sources/declarative/spec/spec.pyunit_tests/sources/declarative/auth/test_rate_limited_multiple_token.pyunit_tests/sources/declarative/spec/test_spec.py
🚧 Files skipped from review as they are similar to previous changes (2)
- airbyte_cdk/sources/declarative/models/declarative_component_schema.py
- airbyte_cdk/sources/declarative/declarative_component_schema.yaml
…cator Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
Reverts the spec.py enum-handling change and its test to the main version; the fix now lands separately via #1103, as requested in review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4758b15
into
main
Summary
Adds a new declarative authenticator,
RateLimitedMultipleTokenAuthenticator, that generalizes source-github'sMultipleTokenAuthenticatorWithRateLimiterso it can be reused by any connector during declarative migrations (context: https://github.com/airbytehq/airbyte-internal-issues/issues/16512).Behavior (per outgoing request, via
AuthBase.__call__):quotas) viaHttpRequestRegexMatchers; a pool with no matchers is the default (generalizes GitHub's hardcoded"graphql" in pathREST-vs-GraphQL split).quota_status_source(e.g. GitHubGET /rate_limit) usingremaining_path/reset_path/limit_pathdpath extraction. Seeding is lazy (first request) and repeated after an exhaustion wait.MIN_EXHAUSTION_WAIT= 5s so stale reset timestamps can't busy-loop the quota endpoint, bounded bymax_wait_time, else raisestransient_error).max(budget_min_reserve, budget_reserve_fraction * limit), delays ofseconds_to_reset / total_remaining(capped at 10s) are injected for graceful degradation instead of a hard wall.Thread safety (new vs. the source-github implementation, required for the concurrent CDK):
RLock;_acquire_callreturns the token chosen under the lock and the request header is built from that value, so a request is always signed with the token whose counter was decremented (no TOCTOU with concurrent rotation). All sleeps occur outside the lock._refresh_lockso only one thread refreshes counters after an exhaustion wait.ModelToComponentFactory.create_rate_limited_multiple_token_authenticatorcaches instances keyed on the model definition, so all streams share one counter set (same idea as the sharedapi_budget).Example manifest usage (source-github):
Changes:
declarative_component_schema.yaml: newRateLimitedMultipleTokenAuthenticator,QuotaStatusSource,TokenQuotadefinitions; added toHttpRequester.authenticatorandSelectiveAuthenticator.authenticatorsunions (models regenerated; new classes applied on top of the checked-in file to avoid unrelated codegen churn).airbyte_cdk/sources/declarative/auth/rate_limited_multiple_token.py: runtime implementation.model_to_component_factory.py:create_rate_limited_multiple_token_authenticatorwith token-string splitting, interpolation, and shared-instance caching.Requested by Daryna Ishchenko (darynaishchenko).
Link to Devin session: https://app.devin.ai/sessions/3ced8f6a11d44ed4abc517221294f767
Requested by: Daryna Ishchenko (@darynaishchenko)
Summary by CodeRabbit