Skip to content

Commit 45a030f

Browse files
lan17claude
andcommitted
docs: add documentation addressing PR review comments
- Add comment explaining thread-local Redis client requirement (async clients are bound to specific event loops) - Complete _exec_fallback docstring with all parameters - Improve RedisValue docstring explaining its role in invalidation - Add class and field docstrings to GCacheGlobalState - Document Fallback type alias - Complete invalidate method docstring with all parameters - Add LocalCache class docstring explaining its role and limitations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7efb8f5 commit 45a030f

4 files changed

Lines changed: 59 additions & 17 deletions

File tree

src/gcache/_internal/cache_interface.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from gcache.config import CacheConfigProvider, CacheLayer, GCacheKey, GCacheKeyConfig
66

7+
#: Async callable that fetches the actual value on cache miss.
8+
#: Invoked by cache implementations when the requested key is not found or is stale.
79
Fallback = Callable[..., Awaitable[Any]]
810

911

@@ -37,14 +39,17 @@ async def delete(self, key: GCacheKey) -> bool:
3739

3840
async def invalidate(self, key_type: str, id: str, future_buffer_ms: int) -> None:
3941
"""
40-
Invalidate all caches matching key_type and id at this point in time.
42+
Invalidate all cache entries matching key_type and id.
4143
42-
Any cache entry that was created before now + future_buffer_ms will be considered invalid.
44+
Sets a watermark timestamp so that any cached value created before
45+
(now + future_buffer_ms) is considered stale on subsequent reads.
4346
44-
:param key_type:
45-
:param id:
46-
:param future_buffer_ms: Invalidate cache into the future. Useful to avoid stale read -> write scenarios.
47-
:return:
47+
:param key_type: The entity type (e.g., 'user', 'project') matching the
48+
key_type used in @cached decorators.
49+
:param id: The entity identifier to invalidate.
50+
:param future_buffer_ms: Extends invalidation window into the future.
51+
Useful to handle race conditions where a read starts before a write
52+
completes but finishes after, preventing caching of stale data.
4853
"""
4954
pass
5055

src/gcache/_internal/local_cache.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,21 @@
1111

1212

1313
class LocalCache(CacheInterface):
14+
"""
15+
In-memory cache layer using TTLCache from cachetools.
16+
17+
Maintains a separate TTLCache instance per use_case, each with a configurable
18+
TTL and a max size of LOCAL_CACHE_MAX_SIZE entries. This is the first layer
19+
in the cache chain, checked before Redis.
20+
21+
Note: LocalCache does not support invalidation (watermarks). If you need
22+
invalidation support, rely on the Redis layer with track_for_invalidation=True.
23+
"""
24+
1425
def __init__(self, cache_config_provider: CacheConfigProvider):
1526
super().__init__(cache_config_provider)
16-
# Dict of usecase -> ttl cache instance.
17-
self.caches: dict[str, TTLCache] = {}
18-
self.lock = asyncio.Lock()
27+
self.caches: dict[str, TTLCache] = {} # use_case -> TTLCache instance
28+
self.lock = asyncio.Lock() # Protects cache creation
1929

2030
async def _get_ttl_cache(self, key: GCacheKey) -> TTLCache:
2131
cache = self.caches.get(key.use_case, None)

src/gcache/_internal/redis_cache.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020
@dataclass(frozen=True, slots=True)
2121
class RedisValue:
2222
"""
23-
Wrap actual payload with created timestamp.
23+
Wrapper around cached payload that includes creation timestamp.
24+
25+
The timestamp enables cache invalidation: when a watermark is set for a key,
26+
any cached value with created_at_ms <= watermark is considered stale.
2427
"""
2528

26-
created_at_ms: int
27-
payload: Any
29+
created_at_ms: int # Unix timestamp in milliseconds when this value was cached
30+
payload: Any # The actual cached data (may be serialized if Serializer is used)
2831

2932

3033
def create_default_redis_client_factory(
@@ -70,6 +73,11 @@ def __init__(
7073
"""
7174
super().__init__(cache_config_provider)
7275
self._client_factory = client_factory
76+
# Thread-local storage is required because async redis-py clients maintain
77+
# internal state (connection pool, pending requests) bound to a specific event loop.
78+
# Since gcache runs sync cached functions in EventLoopThread workers (each with its
79+
# own event loop), sharing a client across threads causes "attached to a different
80+
# event loop" RuntimeError. One client per thread ensures correct event loop binding.
7381
self._thread_local = threading.local()
7482

7583
@property
@@ -91,9 +99,17 @@ async def _exec_fallback(
9199
fallback: Fallback,
92100
) -> Any:
93101
"""
94-
Execute fallback and store it in cache then return it's return value.
95-
:param fallback:
96-
:return:
102+
Execute the fallback function, optionally cache the result, and return it.
103+
104+
The result is stored in cache unless there's an active invalidation window
105+
(watermark_ms is in the future). This prevents caching potentially stale data
106+
that was fetched during an invalidation period.
107+
108+
:param key: Cache key for storing the result.
109+
:param watermark_ms: Invalidation watermark timestamp in milliseconds, or None.
110+
If set and greater than current time, the result is not cached.
111+
:param fallback: Async function that fetches the actual value.
112+
:return: The value returned by the fallback function.
97113
"""
98114
val = await fallback()
99115
if watermark_ms is None or watermark_ms < time.time() * 1e3:

src/gcache/_internal/state.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,23 @@
44
from pydantic import BaseModel, ConfigDict
55

66

7-
# Global state is needed to allow reconfiguration when GCache is instantiated.
8-
# This is fine because GCache is guaranteed to be a singleton.
97
class GCacheGlobalState(BaseModel):
8+
"""
9+
Global configuration state shared across all gcache components.
10+
11+
This state is modified when GCache is instantiated and read by cache implementations,
12+
key builders, and logging throughout the library. Global state is acceptable here
13+
because GCache enforces a singleton pattern.
14+
"""
15+
1016
urn_prefix: str = "urn"
17+
"""Namespace prefix prepended to all cache key URNs (e.g., 'urn:user:123#use_case')."""
18+
1119
logger: Logger | LoggerAdapter = getLogger(__name__)
20+
"""Logger used for debug messages and error reporting throughout gcache."""
21+
1222
gcache_instantiated: bool = False
23+
"""Singleton guard: set to True when GCache is created, prevents duplicate instances."""
1324

1425
model_config = ConfigDict(arbitrary_types_allowed=True)
1526

0 commit comments

Comments
 (0)