Files
insights-python/examples/redis_flag_cache.py
zeekay 0def81f8e8 rebrand: remove all compat aliases, zero posthog refs
- Remove Posthog = Insights alias from __init__.py
- Remove PosthogContextMiddleware alias from django.py
- Remove PostHogTracingProcessor alias from processor.py
- Change $lib from "posthog-python" to "insights-python"
- Change ingestion URLs from posthog.com to insights.hanzo.ai
- Rename all posthog_* kwargs to insights_* across AI wrappers
- Rename __posthog_exception_captured to __insights_exception_captured
- Rename posthog_context_stack contextvar to insights_context_stack
- Rename posthog🎏 Redis prefix to insights🎏
- Rename $$_posthog_redacted_* sentinels to $$_insights_redacted_*
- Remove POSTHOG_MW_* Django settings fallback, X-POSTHOG-* headers
- Rename Prompts(posthog=) param to Prompts(client=)
- Update APP_ENDPOINT to us.insights.hanzo.ai
- Update all tests, examples, docs, mypy config
2026-03-13 20:29:13 -07:00

145 lines
5.0 KiB
Python

"""
Redis-based distributed cache for Insights feature flag definitions.
This example demonstrates how to implement a FlagDefinitionCacheProvider
using Redis for multi-instance deployments (leader election pattern).
Usage:
import redis
from hanzo_insights import Insights
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
cache = RedisFlagCache(redis_client, service_key="my-service")
client = Insights(
"<project_api_key>",
personal_api_key="<personal_api_key>",
flag_definition_cache_provider=cache,
)
Requirements:
pip install redis
"""
import json
import uuid
from hanzo_insights import FlagDefinitionCacheData, FlagDefinitionCacheProvider
from redis import Redis
from typing import Optional
class RedisFlagCache(FlagDefinitionCacheProvider):
"""
A distributed cache for Insights feature flag definitions using Redis.
In a multi-instance deployment (e.g., multiple serverless functions or containers),
we want only ONE instance to poll Insights for flag updates, while all instances
share the cached results. This prevents N instances from making N redundant API calls.
The implementation uses leader election:
- One instance "wins" and becomes responsible for fetching
- Other instances read from the shared cache
- If the leader dies, the lock expires (TTL) and another instance takes over
Uses Lua scripts for atomic operations, following Redis distributed lock best practices:
https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/
"""
LOCK_TTL_MS = 60 * 1000 # 60 seconds, should be longer than the flags poll interval
CACHE_TTL_SECONDS = 60 * 60 * 24 # 24 hours
# Lua script: acquire lock if free, or extend if we own it
_LUA_TRY_LEAD = """
local current = redis.call('GET', KEYS[1])
if current == false then
redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
return 1
elseif current == ARGV[1] then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1
end
return 0
"""
# Lua script: release lock only if we own it
_LUA_STOP_LEAD = """
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
"""
def __init__(self, redis: Redis[str], service_key: str):
"""
Initialize the Redis flag cache.
Args:
redis: A redis-py client instance. Must be configured with
decode_responses=True for correct string handling.
service_key: A unique identifier for this service/environment.
Used to scope Redis keys, allowing multiple services
or environments to share the same Redis instance.
Examples: "my-api-prod", "checkout-service", "staging".
Redis Keys Created:
- insights:flags:{service_key} - Cached flag definitions (JSON)
- insights:flags:{service_key}:lock - Leader election lock
Example:
redis_client = redis.Redis(
host='localhost',
port=6379,
decode_responses=True
)
cache = RedisFlagCache(redis_client, service_key="my-api-prod")
"""
self._redis = redis
self._cache_key = f"insights:flags:{service_key}"
self._lock_key = f"insights:flags:{service_key}:lock"
self._instance_id = str(uuid.uuid4())
self._try_lead = self._redis.register_script(self._LUA_TRY_LEAD)
self._stop_lead = self._redis.register_script(self._LUA_STOP_LEAD)
def get_flag_definitions(self) -> Optional[FlagDefinitionCacheData]:
"""
Retrieve cached flag definitions from Redis.
Returns:
Cached flag definitions if available, None otherwise.
"""
cached = self._redis.get(self._cache_key)
return json.loads(cached) if cached else None
def should_fetch_flag_definitions(self) -> bool:
"""
Determines if this instance should fetch flag definitions from Insights.
Atomically either:
- Acquires the lock if no one holds it, OR
- Extends the lock TTL if we already hold it
Returns:
True if this instance is the leader and should fetch, False otherwise.
"""
result = self._try_lead(
keys=[self._lock_key],
args=[self._instance_id, self.LOCK_TTL_MS],
)
return result == 1
def on_flag_definitions_received(self, data: FlagDefinitionCacheData) -> None:
"""
Store fetched flag definitions in Redis.
Args:
data: The flag definitions to cache.
"""
self._redis.set(self._cache_key, json.dumps(data), ex=self.CACHE_TTL_SECONDS)
def shutdown(self) -> None:
"""
Release leadership if we hold it. Safe to call even if not the leader.
"""
self._stop_lead(keys=[self._lock_key], args=[self._instance_id])