Skip to content

feat: Add async FDv2 data system - #486

Draft
jsonbailey wants to merge 4 commits into
mainfrom
jb/sdk-60/async-fdv2-datasystem
Draft

feat: Add async FDv2 data system#486
jsonbailey wants to merge 4 commits into
mainfrom
jb/sdk-60/async-fdv2-datasystem

Conversation

@jsonbailey

@jsonbailey jsonbailey commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Overview

Part of the async Python SDK work (epic SDK-60). This is the second of two stacked PRs
that add async FDv2 support. It is stacked on #485 (async FDv2 data sources) and
adds the async FDv2 data system (coordinator) and wires it into the async client.

Stacked on #485 — review and merge that PR first. This PR targets
jb/sdk-60/async-fdv2-sources; it will retarget to main once #485 merges.

This is experimental and should not be considered production-ready.

What this PR adds

  • impl/datasystem/async_fdv2.pyAsyncFDv2, the async data system that coordinates
    the async initializers and synchronizers, mirrors the sync FDv2 fallback/recovery
    behavior, and exposes the async data source status and flag tracking.
  • async_client.py wiring:
    • _make_data_system now builds AsyncFDv2 instead of raising NotImplementedError.
    • New _wire_data_source_sessions shares the client's aiohttp session into async data
      source builders so the sources reuse the client's connection pool.
    • __start_up calls the wiring before start when a data system config is present and
      the client is not offline.

Shared refactor

  • impl/datasystem/fdv2_common.py gains fallback_condition and recovery_condition
    as module-level functions, moved out of the sync FDv2 methods so both the sync and
    async data systems share one implementation. Sync fdv2.py now calls them.

Testing

  • LD_SKIP_DATABASE_TESTS=1 uv run pytest ldclient/testing/impl/datasystem/ — 89 passed.
  • ldclient/testing/test_async_client.py — 21 passed.
  • make lint (mypy, isort, pycodestyle) — clean.

Tracked internally: SDK-2870


Note

Overview
Adds async FDv2 so the async Python client can use Flag Delivery V2 when datasystem_config is set, instead of raising NotImplementedError.

AsyncFDv2 coordinates async initializers and synchronizers (main loop, initializer chain, synchronizer fallback/recovery/FDv1 handoff) and exposes flag data via _AsyncStoreView on the shared in-memory store. async_client builds AsyncFDv2 from _make_data_system, and _wire_data_source_sessions injects the client’s shared aiohttp session into async polling/streaming builders before start so FDv2 sources reuse the connection pool.

Refactor: sync and async FDv2 share _FDv2Base in fdv2_common (store, status providers, persistence, data availability) plus fallback_condition / recovery_condition moved out of sync FDv2. Sync fdv2.py delegates to the base and shared helpers.

New test_async_fdv2.py covers start/stop, updates, listeners, two-phase init, synchronizer fallback, FDv1 fallback, and availability/offline behavior.

Reviewed by Cursor Bugbot for commit 938eff1. Bugbot is set up for automated code reviews on this repo. Configure here.

@jsonbailey
jsonbailey force-pushed the jb/sdk-60/async-fdv2-datasystem branch 2 times, most recently from 027941e to b911aa7 Compare August 13, 2026 22:20
@jsonbailey
jsonbailey force-pushed the jb/sdk-60/async-fdv2-datasystem branch from b911aa7 to 35dd8be Compare August 13, 2026 22:32
@jsonbailey
jsonbailey marked this pull request as ready for review August 14, 2026 12:59
@jsonbailey
jsonbailey requested a review from a team as a code owner August 14, 2026 12:59
:param store_writable: Whether the persistent store should be written to
:param disabled: Whether the data system is disabled (offline mode)
"""
super().__init__(config, data_system_config)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sync store I/O blocks event loop

Medium Severity

AsyncFDv2 reuses _FDv2Base, which attaches a synchronous FeatureStore as the active store and persists via sync Store.apply / commit. Those calls run on the asyncio event loop, so a configured persistent store can stall flag evaluation and data updates for the whole async client until the I/O finishes.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 35dd8be. Configure here.

@jsonbailey
jsonbailey force-pushed the jb/sdk-60/async-fdv2-datasystem branch from 35dd8be to c88b6aa Compare August 14, 2026 14:22
return self._store.get(kind, key, lambda x: x)

async def all(self, kind):
return self._store.all(kind, lambda x: x)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Store view skips flag model decode

Medium Severity

_AsyncStoreView returns store items as-is, but get_active_store() can be a persistent FeatureStore that yields raw dicts. The sync client decodes those via _get_store_item before evaluation; the async client feeds store.get / store.all straight into AsyncEvaluator. With a populated persistent store (cached data before network init), evaluations can fail or misbehave because they receive dicts instead of FeatureFlag / segment models.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c88b6aa. Configure here.

@jsonbailey
jsonbailey force-pushed the jb/sdk-60/async-fdv2-datasystem branch from c88b6aa to 1c996d6 Compare August 14, 2026 14:58
Comment thread ldclient/impl/datasystem/async_fdv2.py
@jsonbailey
jsonbailey force-pushed the jb/sdk-60/async-fdv2-datasystem branch from 1c996d6 to 9f6ce38 Compare August 14, 2026 15:05

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ 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 938eff1. Configure here.

if sync_reader is not None:
sync_reader.cancel()

await synchronizer.stop()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reader cancel races synchronizer cleanup

Medium Severity

sync_reader.cancel() runs and then synchronizer.stop() is awaited without waiting for the cancelled reader to finish unwinding first. That races sync() generator cleanup against stop(). For AsyncPollingDataSource, stop() only signals events and relies on sync()'s finally to close the requester, so an aborted cleanup can leak the polling HTTP transport—especially on Python 3.11+, where a pending cancellation re-interrupts awaits inside finally/aclose. Elsewhere, FDv1 async streaming cancels and waits before teardown.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 938eff1. Configure here.

@jsonbailey
jsonbailey marked this pull request as draft August 14, 2026 19:18
Base automatically changed from jb/sdk-60/async-fdv2-sources to main August 14, 2026 20:17
@jsonbailey
jsonbailey force-pushed the jb/sdk-60/async-fdv2-datasystem branch from 938eff1 to cde1b0b Compare August 14, 2026 20:17
@jsonbailey
jsonbailey force-pushed the jb/sdk-60/async-fdv2-datasystem branch from cde1b0b to ef7c261 Compare August 14, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant