feat: Add AsyncLDClient with FDv1 data system and public API - #480
feat: Add AsyncLDClient with FDv1 data system and public API#480jsonbailey wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3cef7b5. Configure here.
| hooks = [] # type: List[AsyncHook] | ||
| with self.__hooks_lock.read(): | ||
| if len(self.__hooks) == 0: | ||
| return await block() |
There was a problem hiding this comment.
Hooks lock held across await
High Severity
When no hooks are registered, __evaluate_with_hooks awaits evaluation while still holding a threading ReadWriteLock read lock. A concurrent add_hook then blocks the event-loop thread on the write lock, so the reader never resumes and the process deadlocks. This is the common default path with an empty hook list.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3cef7b5. Configure here.
| 'variation': detail.variation_index, | ||
| 'reason': detail.reason, | ||
| 'version': flag['version'], | ||
| 'prerequisites': result.prerequisites, |
There was a problem hiding this comment.
Unbound result after flag error
Medium Severity
In all_flags_state, if evaluating a single flag raises, the except path sets detail but still reads result.prerequisites afterward. That raises UnboundLocalError and aborts the whole bootstrap payload instead of continuing with the other flags.
Reviewed by Cursor Bugbot for commit 3cef7b5. Configure here.
Address review findings in the async client evaluation and shutdown paths: - __evaluate_with_hooks held the hooks read lock across `await block()` on the empty-hooks fast path. A concurrent sync add_hook() takes the write lock with a blocking wait, freezing the event-loop thread. Snapshot the hooks under the lock and release it before awaiting, mirroring the sync client's no-await safety. - all_flags_state referenced `result.prerequisites` unconditionally even when a per-flag evaluation raised, causing UnboundLocalError on the first failure or reuse of a neighbor's prerequisites on a later one. Bind the prerequisites safely in both branches so an error degrades only that flag. - __try_execute_stage swallowed asyncio.CancelledError via `except BaseException`, defeating cancellation and shutdown. Re-raise it. - _close_components stopped components in sequence with no isolation, so one failing stop() skipped the rest. Stop each component in its own try/except.


Summary
Adds the async LaunchDarkly client (
AsyncLDClient) built onasyncio/aiohttp, wired to the FDv1 data system, plus the public API surface for constructing it. This is the first extraction slice from the async SDK implementation branch (epic SDK-60).What's included
ldclient/async_client.py—AsyncLDClientwith the FDv1 data system path.ldclient/impl/datasystem/async_fdv1.py— async FDv1 data system.ldclient/impl/datasystem/__init__.py— adds theAsyncDataSystemprotocol.ldclient/__init__.py,ldclient/client.py,ldclient/impl/client_common.py,ldclient/impl/stubs.py— public API / shared plumbing needed by the async client.ldclient/testing/mock_async_components.py,ldclient/testing/stub_util.py— async test doubles.ldclient/testing/test_async_client.py,ldclient/testing/test_sync_async_parity.py— coverage for the async client and sync/async API parity.Held back for a later PR (FDv2)
This slice is FDv1-only. The FDv2 data-system branch in
_make_data_system()(lazy imports ofasync_fdv2/datasourcev2.async_polling/async_streaming) is not included — those modules land in PR 11. For now the FDv2 path raisesNotImplementedError("FDv2 is not yet supported in the async client"). The_wire_data_source_sessionshelper (FDv2-only) was likewise omitted.Follow-ups
Verification
uv run pytest ldclient/testing/test_async_client.py ldclient/testing/test_sync_async_parity.py -q→ 21 passedpycodestyle,isort --check --atomic, andmypyclean on all changed filesasync_fdv2ordatasourcev2.async_*in the treeNote
Medium Risk
Large new experimental client surface touching flag evaluation, networking, and lifecycle; FDv2 is blocked but misconfiguration could surprise adopters until follow-up PRs land.
Overview
Introduces an experimental asyncio-based
AsyncLDClientwith explicitawait start()/close()(or async context manager), mirroring the sync client’s evaluation, events, hooks, and status APIs onAsyncConfigand aiohttp.Wires the client to a new
AsyncFDv1implementation and anAsyncDataSystemprotocol (streaming/polling via existing async datasource types). FDv2 is intentionally unsupported for now (NotImplementedErrorwhendatasystem_configis set).Public plumbing:
ldclientexposesAsyncLDClientvia lazy__getattr__;get_plugin_hooksnow takes a plugin sequence (syncLDClientupdated accordingly). AddsAsyncNullEventProcessor/AsyncNullUpdateProcessorfor offline and no-op paths.Tests:
test_async_client.py(lifecycle, variation, hooks, events) andtest_sync_async_parity.pyto guard sync/async API surface drift.Reviewed by Cursor Bugbot for commit 3cef7b5. Bugbot is set up for automated code reviews on this repo. Configure here.