From b81e2ae6dc6c4a0cff3b8eda47e51465fe8580eb Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Wed, 27 May 2026 13:01:03 -0400 Subject: [PATCH 1/3] docs: document async Python flag cache providers --- ...stributed-environments-common-patterns.mdx | 7 ++++- .../distributed-environments-python.mdx | 29 ++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-common-patterns.mdx b/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-common-patterns.mdx index 8fee310b120f..e023d5ef73ba 100644 --- a/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-common-patterns.mdx +++ b/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-common-patterns.mdx @@ -13,7 +13,12 @@ The recommended pattern: #### Redis example -A complete working example written in Python using Redis with distributed locking is available in the [posthog-python repository](https://github.com/PostHog/posthog-python/blob/master/examples/redis_flag_cache.py). It implements the locking pattern described above. +Complete working examples written in Python using Redis with distributed locking are available in the posthog-python repository: + +- [Synchronous Redis cache provider](https://github.com/PostHog/posthog-python/blob/master/examples/redis_flag_cache.py) +- [Async Redis cache provider](https://github.com/PostHog/posthog-python/blob/master/examples/async_redis_flag_cache.py) for async-first applications using `redis.asyncio` + +They implement the locking pattern described above. ### Caches without locking diff --git a/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx b/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx index 14c13b8e7c74..861d6e8af4e3 100644 --- a/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx +++ b/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx @@ -11,24 +11,37 @@ from posthog import FlagDefinitionCacheProvider, FlagDefinitionCacheData To create a custom cache, implement the `FlagDefinitionCacheProvider` protocol: ```python +from typing import Awaitable, Optional, Protocol, Union + + class FlagDefinitionCacheProvider(Protocol): - def get_flag_definitions(self) -> Optional[FlagDefinitionCacheData]: + def get_flag_definitions( + self, + ) -> Union[ + Optional[FlagDefinitionCacheData], Awaitable[Optional[FlagDefinitionCacheData]] + ]: """Retrieve cached flag definitions.""" ... - def should_fetch_flag_definitions(self) -> bool: + def should_fetch_flag_definitions(self) -> Union[bool, Awaitable[bool]]: """Determine if this instance should fetch new definitions.""" ... - def on_flag_definitions_received(self, data: FlagDefinitionCacheData) -> None: + def on_flag_definitions_received( + self, data: FlagDefinitionCacheData + ) -> Optional[Awaitable[None]]: """Store definitions after a successful fetch.""" ... - def shutdown(self) -> None: + def shutdown(self) -> Optional[Awaitable[None]]: """Clean up resources on shutdown.""" ... ``` +You can implement these methods as synchronous methods or as `async def` methods. If a method returns an awaitable, the SDK runs it to completion before continuing. + +> **Note:** If you use an async client that is tied to an event loop, such as `redis.asyncio`, use a client dedicated to this cache provider. The SDK runs async cache provider methods on its own background event loop. + When the SDK fetches flag definitions from the API, it passes a `FlagDefinitionCacheData` object to `on_flag_definitions_received()` for you to store: ```python @@ -42,10 +55,10 @@ class FlagDefinitionCacheData(TypedDict): | Method | Purpose | Return value | | :----- | :------ | :----------- | -| `get_flag_definitions()` | Retrieve cached definitions. Called when the poller refreshes. | Cached data, or `None` if cache is empty | -| `should_fetch_flag_definitions()` | Decide if this instance should fetch. Use for distributed coordination (e.g., locks). | `True` to fetch, `False` to skip | -| `on_flag_definitions_received(data)` | Store definitions after a successful API fetch. | None | -| `shutdown()` | Release locks, close connections, clean up resources. | None | +| `get_flag_definitions()` | Retrieve cached definitions. Called when the poller refreshes. | Cached data, `None` if cache is empty, or an awaitable resolving to either | +| `should_fetch_flag_definitions()` | Decide if this instance should fetch. Use for distributed coordination (e.g., locks). | `True` to fetch, `False` to skip, or an awaitable resolving to a boolean | +| `on_flag_definitions_received(data)` | Store definitions after a successful API fetch. | `None` or an awaitable resolving to `None` | +| `shutdown()` | Release locks, close connections, clean up resources. | `None` or an awaitable resolving to `None` | > **Note:** All methods may throw errors. The SDK catches and logs them gracefully, ensuring cache provider errors never break flag evaluation. From bdff2c70301c01e007162b549568ef66228b69e3 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Wed, 27 May 2026 13:03:19 -0400 Subject: [PATCH 2/3] docs: clarify Python async cache provider wording --- .../_snippets/distributed-environments-python.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx b/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx index 861d6e8af4e3..85e42c5431e7 100644 --- a/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx +++ b/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx @@ -38,7 +38,7 @@ class FlagDefinitionCacheProvider(Protocol): ... ``` -You can implement these methods as synchronous methods or as `async def` methods. If a method returns an awaitable, the SDK runs it to completion before continuing. +You can implement these methods as either sync or async methods. If a method returns an awaitable, the SDK runs it to completion before continuing. > **Note:** If you use an async client that is tied to an event loop, such as `redis.asyncio`, use a client dedicated to this cache provider. The SDK runs async cache provider methods on its own background event loop. From 207de575d91dd4b2d9b8b437244fa0f8a7398305 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Fri, 5 Jun 2026 09:36:36 -0400 Subject: [PATCH 3/3] docs: clean up some language --- .../distributed-environments-common-patterns.mdx | 6 +++--- .../_snippets/distributed-environments-python.mdx | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-common-patterns.mdx b/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-common-patterns.mdx index e023d5ef73ba..543bb77634fb 100644 --- a/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-common-patterns.mdx +++ b/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-common-patterns.mdx @@ -13,10 +13,10 @@ The recommended pattern: #### Redis example -Complete working examples written in Python using Redis with distributed locking are available in the posthog-python repository: +Complete working examples written in Python using Redis with distributed locking are available in the PostHog Python repository: -- [Synchronous Redis cache provider](https://github.com/PostHog/posthog-python/blob/master/examples/redis_flag_cache.py) -- [Async Redis cache provider](https://github.com/PostHog/posthog-python/blob/master/examples/async_redis_flag_cache.py) for async-first applications using `redis.asyncio` +- [Synchronous Redis cache provider](https://github.com/PostHog/posthog-python/blob/a91ca2b479b8bd74b1192a67887a1396cf92e385/examples/redis_flag_cache.py) +- [Async Redis cache provider](https://github.com/PostHog/posthog-python/blob/a91ca2b479b8bd74b1192a67887a1396cf92e385/examples/async_redis_flag_cache.py) for async-first applications using `redis.asyncio` They implement the locking pattern described above. diff --git a/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx b/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx index 85e42c5431e7..5c271580348c 100644 --- a/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx +++ b/contents/docs/feature-flags/local-evaluation/_snippets/distributed-environments-python.mdx @@ -38,7 +38,7 @@ class FlagDefinitionCacheProvider(Protocol): ... ``` -You can implement these methods as either sync or async methods. If a method returns an awaitable, the SDK runs it to completion before continuing. +You can implement these methods as either sync or async methods. If a method is async, the SDK waits for it to finish before continuing. > **Note:** If you use an async client that is tied to an event loop, such as `redis.asyncio`, use a client dedicated to this cache provider. The SDK runs async cache provider methods on its own background event loop. @@ -55,12 +55,12 @@ class FlagDefinitionCacheData(TypedDict): | Method | Purpose | Return value | | :----- | :------ | :----------- | -| `get_flag_definitions()` | Retrieve cached definitions. Called when the poller refreshes. | Cached data, `None` if cache is empty, or an awaitable resolving to either | -| `should_fetch_flag_definitions()` | Decide if this instance should fetch. Use for distributed coordination (e.g., locks). | `True` to fetch, `False` to skip, or an awaitable resolving to a boolean | -| `on_flag_definitions_received(data)` | Store definitions after a successful API fetch. | `None` or an awaitable resolving to `None` | -| `shutdown()` | Release locks, close connections, clean up resources. | `None` or an awaitable resolving to `None` | +| `get_flag_definitions()` | Retrieve cached definitions. Called when the poller refreshes. | Cached data or `None`; async implementations return the same after being awaited | +| `should_fetch_flag_definitions()` | Decide if this instance should fetch. Use for distributed coordination (e.g., locks). | `True` to fetch or `False` to skip; async implementations return the same after being awaited | +| `on_flag_definitions_received(data)` | Store definitions after a successful API fetch. | `None`; async implementations return `None` after being awaited | +| `shutdown()` | Release locks, close connections, clean up resources. | `None`; async implementations return `None` after being awaited | -> **Note:** All methods may throw errors. The SDK catches and logs them gracefully, ensuring cache provider errors never break flag evaluation. +> **Note:** It is safe for cache provider implementations to raise exceptions. The SDK catches provider exceptions and continues without re-raising them: `should_fetch_flag_definitions()` falls back to fetching, `get_flag_definitions()` falls back to the API, and `on_flag_definitions_received()` or `shutdown()` failures do not stop flag evaluation or shutdown. ## Using your cache provider