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..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,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/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. ### 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..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 @@ -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 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. + 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,12 +55,12 @@ 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 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