feat: Add async persistent feature store foundation and Redis adapter - #488
Merged
Conversation
keelerm84
approved these changes
Aug 13, 2026
keelerm84
approved these changes
Aug 13, 2026
This was referenced Aug 14, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
BEGIN_COMMIT_OVERRIDE
feat: Add async persistent feature store foundation and Redis adapter
fix: Cap the Redis feature store upsert retry loop at 10 attempts
END_COMMIT_OVERRRIDE
This PR adds the foundation for async persistent feature stores to the async server SDK, with Redis as the first adapter. Until now the async SDK had only an in-memory feature store and an async Redis Big Segment store; there was no async persistent feature store and no async caching layer. This provides that foundation, designed to be adapter-agnostic so DynamoDB and Consul adapters can be stacked on top later.
What this PR adds
AsyncFeatureStoreCoreprotocol (ldclient/interfaces.py) — the async equivalent ofFeatureStoreCore: the simplified, database-specific surface (get_internal,get_all_internal,init_internal,upsert_internal,initialized_internal) that an adapter implements. Optionalis_available/closeare detected by the wrapper rather than declared, matching the sync core._CachingStoreWrapperBase(ldclient/feature_store_helpers.py) — a shared, sans-I/O cache base holding the cache setup and the cache bookkeeping overself._cache. Both the syncCachingStoreWrapperand the new async wrapper subclass it, so the caching logic lives once. Each subclass owns its precisely-typed_coreand its I/O-shaped methods.AsyncCachingStoreWrapper(ldclient/async_feature_store_helpers.py) — implementsAsyncFeatureStoreby wrapping anAsyncFeatureStoreCoreplus an async-safe cache. Mirrors the sync cache semantics (disabled / TTL / infinite; init/get/all/upsert/delete; decoded model objects), awaiting the core.upsert/deletereturn aboolper the async store contract;initializedreflects whetherinithas completed in this process (the interface property is synchronous, so it cannot run a coroutine store query).ldclient/impl/integrations/redis/async_redis_feature_store.py) —_AsyncRedisFeatureStoreCore, mirroring the sync_RedisFeatureStoreCoreusingredis.asyncio(the same client the async Big Segment store uses), including the WATCH/MULTI optimistic-lock upsert with retry.Redis.async_feature_store(...)(ldclient/integrations/__init__.py), following the existingRedis.async_big_segment_storenaming.{prefix}:featureshash +{prefix}:$initedmarker), so an async and a synchronous SDK can share one Redis instance.Testing
ldclient/testing/test_async_feature_store_helpers.py) using an in-memory mock core — these run without Redis.ldclient/testing/integrations/test_async_redis_feature_store.py) — gated byLD_SKIP_DATABASE_TESTS, mirroring the existing async Redis Big Segment tests, plus the two upsert race-condition tests adapted to async.LD_SKIP_DATABASE_TESTS=1 python -m pytest ldclient/testing→ 1363 passed, 279 skipped. The Redis code paths were also exercised locally againstfakeredis(init/get/all/upsert incl. version-rejection/delete/is_available and both WATCH-race scenarios).Also caps the sync Redis store
The sync Redis feature store (
redis_feature_store.py) had the same unbounded WATCH-retry loop. This PR caps it too (10 attempts, raise on exhaustion) — note this is a behavior change to already-released code: an unbounded spin under sustained contention becomes a surfaced store error.Tracked internally: SDK-2905
Note
Overview
Adds an experimental async persistent feature store stack for
AsyncLDClient, mirroring the syncFeatureStoreCore+CachingStoreWrapperpattern.Shared caching:
CachingStoreWrappernow subclasses_CachingStoreWrapperBase, which centralizes cache setup,disable_cache, and the get/put/init/upsert cache helpers; sync methods delegate to those helpers instead of duplicating logic.Async surface: New
AsyncFeatureStoreCoreprotocol andAsyncCachingStoreWrapperimplementAsyncFeatureStorewith the same caching semantics as sync (includingupsert/deletereturning whether the write applied).initializedonly reflects whetherinitcompleted in-process, since the property cannot await a store query.Redis:
_AsyncRedisFeatureStoreCoreusesredis.asynciowith the same key layout as the sync Redis store (WATCH/MULTI upsert with bounded retries).Redis.async_feature_store(...)wires core + wrapper for use in client config.Tests: Mock-core unit tests for the async wrapper and DB-gated integration tests for async Redis (prefix isolation, upsert races, availability).
Reviewed by Cursor Bugbot for commit 1f4d0f7. Bugbot is set up for automated code reviews on this repo. Configure here.