Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions growthbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
BackoffStrategy
)

from .cache_interfaces import (
AbstractFeatureCache,
AbstractAsyncFeatureCache,
InMemoryAsyncFeatureCache
)

# Plugin support
from .plugins import (
GrowthBookTrackingPlugin,
Expand Down
109 changes: 109 additions & 0 deletions growthbook/cache_interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import asyncio
from abc import abstractmethod, ABC
from time import time
from typing import Optional, Dict

class AbstractFeatureCache(ABC):
@abstractmethod
def get(self, key: str) -> Optional[Dict]:
pass

@abstractmethod
def set(self, key: str, value: Dict, ttl: int) -> None:
pass

def clear(self) -> None:
pass

class AbstractAsyncFeatureCache(ABC):
"""Abstract base class for async feature caching implementations"""

@abstractmethod
async def get(self, key: str) -> Optional[Dict]:
"""
Retrieve cached features by key.

Args:
key: Cache key

Returns:
Cached dictionary or None if not found/expired
"""
pass

@abstractmethod
async def set(self, key: str, value: Dict, ttl: int) -> None:
"""
Store features in cache with TTL.

Args:
key: Cache key
value: Features dictionary to cache
ttl: Time to live in seconds
"""
pass

async def clear(self) -> None:
"""Clear all cached entries (optional to override)"""
pass

class CacheEntry(object):
def __init__(self, value: Dict, ttl: int) -> None:
self.value = value
self.ttl = ttl
self.expires = time() + ttl

def update(self, value: Dict):
self.value = value
self.expires = time() + self.ttl


class InMemoryFeatureCache(AbstractFeatureCache):
def __init__(self) -> None:
self.cache: Dict[str, CacheEntry] = {}

def get(self, key: str) -> Optional[Dict]:
if key in self.cache:
entry = self.cache[key]
if entry.expires >= time():
return entry.value
return None

def set(self, key: str, value: Dict, ttl: int) -> None:
if key in self.cache:
self.cache[key].update(value)
else:
self.cache[key] = CacheEntry(value, ttl)

def clear(self) -> None:
self.cache.clear()


class InMemoryAsyncFeatureCache(AbstractAsyncFeatureCache):
"""
Async in-memory cache implementation.
Uses the same CacheEntry structure but with async interface.
"""

def __init__(self) -> None:
self._cache: Dict[str, CacheEntry] = {}
self._lock = asyncio.Lock()

async def get(self, key: str) -> Optional[Dict]:
async with self._lock:
if key in self._cache:
entry = self._cache[key]
if entry.expires >= time():
return entry.value
return None

async def set(self, key: str, value: Dict, ttl: int) -> None:
async with self._lock:
if key in self._cache:
self._cache[key].update(value)
else:
self._cache[key] = CacheEntry(value, ttl)

async def clear(self) -> None:
async with self._lock:
self._cache.clear()
5 changes: 4 additions & 1 deletion growthbook/common_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import Any, Callable, Dict, List, Optional, Union, Set, Tuple
from enum import Enum
from abc import ABC, abstractmethod
from .cache_interfaces import AbstractFeatureCache, AbstractAsyncFeatureCache

class VariationMeta(TypedDict):
key: str
Expand Down Expand Up @@ -396,7 +397,7 @@ def get_all_assignments(self, attributes: Dict[str, str]) -> Dict[str, Dict]:
return docs

@dataclass
class StackContext:
class StackContext:
id: Optional[str] = None
evaluated_features: Set[str] = field(default_factory=set)

Expand Down Expand Up @@ -433,6 +434,8 @@ class Options:
tracking_plugins: Optional[List[Any]] = None
http_connect_timeout: Optional[int] = None
http_read_timeout: Optional[int] = None
cache: Optional[AbstractFeatureCache] = None
async_cache: Optional[AbstractAsyncFeatureCache] = None


@dataclass
Expand Down
Loading