BaseExperimentationProvider: ttl on item level (#4540)

This commit is contained in:
Stanislav Novosad
2026-01-23 16:49:07 -07:00
committed by GitHub
parent fde56004e9
commit 43d8020792

View File

@@ -11,7 +11,7 @@ EXPERIMENTATION_CACHE_MAX_SIZE = 100000 # Max entries per cache
class BaseExperimentationProvider(ABC): class BaseExperimentationProvider(ABC):
def __init__(self) -> None: def __init__(self) -> None:
# feature_name -> distinct_id -> result with TTL-based expiration # Cache with composite key (feature_name, distinct_id) for per-entry TTL expiration
self.result_map: TTLCache = TTLCache(maxsize=EXPERIMENTATION_CACHE_MAX_SIZE, ttl=EXPERIMENTATION_CACHE_TTL) self.result_map: TTLCache = TTLCache(maxsize=EXPERIMENTATION_CACHE_MAX_SIZE, ttl=EXPERIMENTATION_CACHE_TTL)
self.variant_map: TTLCache = TTLCache(maxsize=EXPERIMENTATION_CACHE_MAX_SIZE, ttl=EXPERIMENTATION_CACHE_TTL) self.variant_map: TTLCache = TTLCache(maxsize=EXPERIMENTATION_CACHE_MAX_SIZE, ttl=EXPERIMENTATION_CACHE_TTL)
self.payload_map: TTLCache = TTLCache(maxsize=EXPERIMENTATION_CACHE_MAX_SIZE, ttl=EXPERIMENTATION_CACHE_TTL) self.payload_map: TTLCache = TTLCache(maxsize=EXPERIMENTATION_CACHE_MAX_SIZE, ttl=EXPERIMENTATION_CACHE_TTL)
@@ -23,15 +23,14 @@ class BaseExperimentationProvider(ABC):
async def is_feature_enabled_cached( async def is_feature_enabled_cached(
self, feature_name: str, distinct_id: str, properties: dict | None = None self, feature_name: str, distinct_id: str, properties: dict | None = None
) -> bool: ) -> bool:
if feature_name not in self.result_map: cache_key = (feature_name, distinct_id)
self.result_map[feature_name] = {} if cache_key not in self.result_map:
if distinct_id not in self.result_map[feature_name]:
feature_flag_value = await self.is_feature_enabled(feature_name, distinct_id, properties) feature_flag_value = await self.is_feature_enabled(feature_name, distinct_id, properties)
self.result_map[feature_name][distinct_id] = feature_flag_value self.result_map[cache_key] = feature_flag_value
if feature_flag_value: if feature_flag_value:
LOG.info("Feature flag is enabled", flag=feature_name, distinct_id=distinct_id) LOG.info("Feature flag is enabled", flag=feature_name, distinct_id=distinct_id)
return self.result_map[feature_name][distinct_id] return self.result_map[cache_key]
@abstractmethod @abstractmethod
async def get_value(self, feature_name: str, distinct_id: str, properties: dict | None = None) -> str | None: async def get_value(self, feature_name: str, distinct_id: str, properties: dict | None = None) -> str | None:
@@ -43,27 +42,25 @@ class BaseExperimentationProvider(ABC):
async def get_value_cached(self, feature_name: str, distinct_id: str, properties: dict | None = None) -> str | None: async def get_value_cached(self, feature_name: str, distinct_id: str, properties: dict | None = None) -> str | None:
"""Get the value of a feature.""" """Get the value of a feature."""
if feature_name not in self.variant_map: cache_key = (feature_name, distinct_id)
self.variant_map[feature_name] = {} if cache_key not in self.variant_map:
if distinct_id not in self.variant_map[feature_name]:
variant = await self.get_value(feature_name, distinct_id, properties) variant = await self.get_value(feature_name, distinct_id, properties)
self.variant_map[feature_name][distinct_id] = variant self.variant_map[cache_key] = variant
if variant: if variant:
LOG.info("Feature is found", flag=feature_name, distinct_id=distinct_id, variant=variant) LOG.info("Feature is found", flag=feature_name, distinct_id=distinct_id, variant=variant)
return self.variant_map[feature_name][distinct_id] return self.variant_map[cache_key]
async def get_payload_cached( async def get_payload_cached(
self, feature_name: str, distinct_id: str, properties: dict | None = None self, feature_name: str, distinct_id: str, properties: dict | None = None
) -> str | None: ) -> str | None:
"""Get the payload for a feature flag if it exists.""" """Get the payload for a feature flag if it exists."""
if feature_name not in self.payload_map: cache_key = (feature_name, distinct_id)
self.payload_map[feature_name] = {} if cache_key not in self.payload_map:
if distinct_id not in self.payload_map[feature_name]:
payload = await self.get_payload(feature_name, distinct_id, properties) payload = await self.get_payload(feature_name, distinct_id, properties)
self.payload_map[feature_name][distinct_id] = payload self.payload_map[cache_key] = payload
if payload: if payload:
LOG.info("Feature payload is found", flag=feature_name, distinct_id=distinct_id, payload=payload) LOG.info("Feature payload is found", flag=feature_name, distinct_id=distinct_id, payload=payload)
return self.payload_map[feature_name][distinct_id] return self.payload_map[cache_key]
class NoOpExperimentationProvider(BaseExperimentationProvider): class NoOpExperimentationProvider(BaseExperimentationProvider):