-
Notifications
You must be signed in to change notification settings - Fork 47
feat: Add AsyncLDClient with FDv1 data system and public API #480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
ffcdff1
feat: Add AsyncLDClient with FDv1 data system and public API
jsonbailey 36b72f1
fix: Snapshot hooks before awaiting to avoid an event-loop deadlock
jsonbailey f787b32
fix: Keep AsyncLDClient out of __all__ so star-import avoids aiohttp
jsonbailey 374b6cf
test: Cover all_flags_state normal and evaluator-raises paths
jsonbailey 6540e68
refactor: Drop the hooks lock and rely on single-event-loop atomicity
jsonbailey 404d48c
refactor: Close components sequentially to match sync (drop per-compo…
jsonbailey 758045e
refactor: Bind all_flags_state prerequisites in both eval branches
jsonbailey cd6bd60
docs: Simplify async client comments (hooks snapshot, lazy load, all_…
jsonbailey aa524f3
docs: Trim restating comments and consolidate parity allowlist notes
jsonbailey 6c9ffeb
refactor: Drop _get_store_item dict shim; async stores return decoded…
jsonbailey f3a578f
test: Decode force_set items so async store reads return model objects
jsonbailey 378f27c
fix: Log and no-op instead of raising when start() is called on a clo…
jsonbailey 0fc7dac
feat: Make the flag tracker available before start()
jsonbailey 3e9be62
refactor: Build client components in __init__ and create the session …
jsonbailey bcd5bdd
fix: Import error_reason from evaluator_common
jsonbailey 4663828
fix: Address Bugbot review findings on the async client
jsonbailey 948902c
fix: Clean up a cancelled start() (CancelledError is a BaseException)
jsonbailey 3958058
refactor: Refine async client start/close lifecycle and hook registra…
jsonbailey 4e6f1bf
refactor: Simplify async client close() teardown
jsonbailey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| from typing import Any, Callable, Optional | ||
|
|
||
| from ldclient.async_config import AsyncConfig | ||
| from ldclient.impl.aio.concurrency import AsyncEvent | ||
| from ldclient.impl.aio.transport import AsyncHTTPTransport, AsyncSSEFactory | ||
| from ldclient.impl.datasource.async_feature_requester import ( | ||
| AsyncFeatureRequesterImpl | ||
| ) | ||
| from ldclient.impl.datasource.async_polling import AsyncPollingUpdateProcessor | ||
| from ldclient.impl.datasource.async_status import AsyncDataSourceUpdateSinkImpl | ||
| from ldclient.impl.datasource.async_streaming import ( | ||
| AsyncStreamingUpdateProcessor | ||
| ) | ||
| from ldclient.impl.datasource.status import DataSourceStatusProviderImpl | ||
| from ldclient.impl.datastore.status import ( | ||
| DataStoreStatusProviderImpl, | ||
| DataStoreUpdateSinkImpl | ||
| ) | ||
| from ldclient.impl.datasystem import ( | ||
| AsyncDataSystem, | ||
| DataAvailability, | ||
| DiagnosticAccumulator | ||
| ) | ||
| from ldclient.impl.listeners import Listeners | ||
| from ldclient.impl.stubs import AsyncNullUpdateProcessor | ||
| from ldclient.impl.util import log | ||
| from ldclient.interfaces import ( | ||
| AsyncFeatureStore, | ||
| AsyncReadOnlyStore, | ||
| AsyncUpdateProcessor, | ||
| DataSourceStatusProvider, | ||
| DataStoreStatusProvider | ||
| ) | ||
|
|
||
|
|
||
| class AsyncFDv1(AsyncDataSystem): | ||
| """ | ||
| AsyncFDv1 provides the v1 data source and store behavior through the | ||
| AsyncDataSystem interface. It is the async version of | ||
| :class:`ldclient.impl.datasystem.fdv1.FDv1`. Unlike the sync side, it uses | ||
| the feature store directly and does not wrap it for persistent-store status | ||
| monitoring. | ||
| """ | ||
|
|
||
| def __init__(self, config: AsyncConfig, store: AsyncFeatureStore, session_provider: Callable[[], Any]): | ||
| self._config = config | ||
| self._store = store | ||
| # The client creates the aiohttp session lazily inside the loop; the data | ||
| # source resolves it here when it builds its network processor at start(). | ||
| self._session_provider = session_provider | ||
|
|
||
| # Set up data store status tracking (no store wrapper) | ||
| self._data_store_listeners = Listeners() | ||
| self._data_store_update_sink = DataStoreUpdateSinkImpl( | ||
| self._data_store_listeners | ||
| ) | ||
| # The provider only calls the store's monitoring methods, which the async | ||
| # store also has, so the sync-typed signature is fine. | ||
| self._data_store_status_provider_impl = DataStoreStatusProviderImpl( | ||
| self._store, self._data_store_update_sink # type: ignore[arg-type] | ||
| ) | ||
|
|
||
| # Set up the data source status tracking and listeners | ||
| self._data_source_listeners = Listeners() | ||
| self._flag_change_listeners = Listeners() | ||
| self._data_source_update_sink = AsyncDataSourceUpdateSinkImpl( | ||
| self._store, | ||
| self._data_source_listeners, | ||
| self._flag_change_listeners, | ||
| ) | ||
| self._data_source_status_provider_impl = DataSourceStatusProviderImpl( | ||
| self._data_source_listeners, self._data_source_update_sink | ||
| ) | ||
|
|
||
| # v1 processors read the sink from the config for status updates. The config | ||
| # attribute is typed as the sync sink, but the async sink has the same methods. | ||
| self._config._data_source_update_sink = self._data_source_update_sink # type: ignore[assignment] | ||
|
|
||
| # Update processor created in start(), because it needs the ready event | ||
| self._update_processor: Optional[AsyncUpdateProcessor] = None | ||
|
|
||
| # Diagnostic accumulator provided by client for streaming metrics | ||
| self._diagnostic_accumulator: Optional[DiagnosticAccumulator] = None | ||
|
|
||
| def start(self, set_on_ready: AsyncEvent): | ||
| """ | ||
| Starts the v1 update processor and returns immediately. The provided | ||
| event is set by the processor upon first successful initialization or | ||
| upon permanent failure. | ||
| """ | ||
| update_processor = self._make_update_processor( | ||
| self._config, self._store, set_on_ready | ||
| ) | ||
| self._update_processor = update_processor | ||
| update_processor.start() | ||
|
|
||
| async def stop(self): | ||
| if self._update_processor is not None: | ||
| await self._update_processor.stop() | ||
|
|
||
| @property | ||
| def store(self) -> AsyncReadOnlyStore: | ||
| return self._store | ||
|
|
||
| def set_diagnostic_accumulator(self, diagnostic_accumulator: DiagnosticAccumulator): | ||
| """ | ||
| Sets the diagnostic accumulator for streaming initialization metrics. | ||
| This should be called before start() to ensure metrics are collected. | ||
| """ | ||
| self._diagnostic_accumulator = diagnostic_accumulator | ||
|
|
||
| @property | ||
| def data_source_status_provider(self) -> DataSourceStatusProvider: | ||
| return self._data_source_status_provider_impl | ||
|
|
||
| @property | ||
| def data_store_status_provider(self) -> DataStoreStatusProvider: | ||
| return self._data_store_status_provider_impl | ||
|
|
||
| @property | ||
| def flag_change_listeners(self) -> Listeners: | ||
| return self._flag_change_listeners | ||
|
|
||
| @property | ||
| def data_availability(self) -> DataAvailability: | ||
| if self._config.offline: | ||
| return DataAvailability.DEFAULTS | ||
|
|
||
| if self._update_processor is not None and self._update_processor.initialized(): | ||
| return DataAvailability.REFRESHED | ||
|
|
||
| if self._store.initialized: | ||
| return DataAvailability.CACHED | ||
|
|
||
| return DataAvailability.DEFAULTS | ||
|
|
||
| @property | ||
| def target_availability(self) -> DataAvailability: | ||
| if self._config.offline: | ||
| return DataAvailability.DEFAULTS | ||
| # In LDD mode or normal connected modes, the ideal is to be refreshed | ||
| return DataAvailability.REFRESHED | ||
|
|
||
| def _make_update_processor(self, config: AsyncConfig, store: AsyncFeatureStore, ready: AsyncEvent): | ||
| # Mirrors FDv1._make_update_processor but builds the async processors | ||
| if config.update_processor_class: | ||
| log.info("Using user-specified update processor: " + str(config.update_processor_class)) | ||
| return config.update_processor_class(config, store, ready) | ||
|
|
||
| if config.offline or config.use_ldd: | ||
| return AsyncNullUpdateProcessor(config, store, ready) | ||
|
|
||
| if config.stream: | ||
| return AsyncStreamingUpdateProcessor( | ||
| config, | ||
| store, | ||
| ready, | ||
| self._diagnostic_accumulator, | ||
| AsyncSSEFactory(config, session=self._session_provider(), proxy=config.http.http_proxy), | ||
| ) | ||
|
|
||
| log.info("Disabling streaming API") | ||
| log.warning("You should only disable the streaming API if instructed to do so by LaunchDarkly support") | ||
|
|
||
| if config.feature_requester_class: | ||
| feature_requester = config.feature_requester_class(config) | ||
| else: | ||
| feature_requester = AsyncFeatureRequesterImpl( | ||
| config, | ||
| AsyncHTTPTransport(config, client=self._session_provider()), | ||
| ) | ||
| return AsyncPollingUpdateProcessor(config, feature_requester, store, ready) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.