Skip to content

feat: Add async persistent feature store foundation and Redis adapter - #488

Merged
jsonbailey merged 3 commits into
mainfrom
jb/sdk-2905/async-persist-redis
Aug 13, 2026
Merged

feat: Add async persistent feature store foundation and Redis adapter#488
jsonbailey merged 3 commits into
mainfrom
jb/sdk-2905/async-persist-redis

Conversation

@jsonbailey

@jsonbailey jsonbailey commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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

  • AsyncFeatureStoreCore protocol (ldclient/interfaces.py) — the async equivalent of FeatureStoreCore: the simplified, database-specific surface (get_internal, get_all_internal, init_internal, upsert_internal, initialized_internal) that an adapter implements. Optional is_available/close are 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 over self._cache. Both the sync CachingStoreWrapper and the new async wrapper subclass it, so the caching logic lives once. Each subclass owns its precisely-typed _core and its I/O-shaped methods.
  • AsyncCachingStoreWrapper (ldclient/async_feature_store_helpers.py) — implements AsyncFeatureStore by wrapping an AsyncFeatureStoreCore plus an async-safe cache. Mirrors the sync cache semantics (disabled / TTL / infinite; init/get/all/upsert/delete; decoded model objects), awaiting the core. upsert/delete return a bool per the async store contract; initialized reflects whether init has completed in this process (the interface property is synchronous, so it cannot run a coroutine store query).
  • Async Redis adapter (ldclient/impl/integrations/redis/async_redis_feature_store.py) — _AsyncRedisFeatureStoreCore, mirroring the sync _RedisFeatureStoreCore using redis.asyncio (the same client the async Big Segment store uses), including the WATCH/MULTI optimistic-lock upsert with retry.
  • Integration entry pointRedis.async_feature_store(...) (ldclient/integrations/__init__.py), following the existing Redis.async_big_segment_store naming.
  • Same Redis key layout as the sync store ({prefix}:features hash + {prefix}:$inited marker), so an async and a synchronous SDK can share one Redis instance.

Testing

  • New non-DB unit tests for the caching-wrapper logic (ldclient/testing/test_async_feature_store_helpers.py) using an in-memory mock core — these run without Redis.
  • New DB-gated integration tests for the async Redis feature store (ldclient/testing/integrations/test_async_redis_feature_store.py) — gated by LD_SKIP_DATABASE_TESTS, mirroring the existing async Redis Big Segment tests, plus the two upsert race-condition tests adapted to async.
  • Full unit suite green: LD_SKIP_DATABASE_TESTS=1 python -m pytest ldclient/testing1363 passed, 279 skipped. The Redis code paths were also exercised locally against fakeredis (init/get/all/upsert incl. version-rejection/delete/is_available and both WATCH-race scenarios).
  • mypy, isort, and pycodestyle clean on the touched files.

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 sync FeatureStoreCore + CachingStoreWrapper pattern.

Shared caching: CachingStoreWrapper now 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 AsyncFeatureStoreCore protocol and AsyncCachingStoreWrapper implement AsyncFeatureStore with the same caching semantics as sync (including upsert/delete returning whether the write applied). initialized only reflects whether init completed in-process, since the property cannot await a store query.

Redis: _AsyncRedisFeatureStoreCore uses redis.asyncio with 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.

@jsonbailey
jsonbailey requested a review from a team as a code owner August 13, 2026 14:06
Comment thread ldclient/impl/integrations/redis/async_redis_feature_store.py Outdated
@jsonbailey
jsonbailey merged commit f9c76ee into main Aug 13, 2026
17 checks passed
@jsonbailey
jsonbailey deleted the jb/sdk-2905/async-persist-redis branch August 13, 2026 16:08
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.

2 participants