@@ -8,13 +8,21 @@ gcache is a fine-grained caching library with multi-layer support (local + Redis
88
99```
1010src/gcache/
11- ├── __init__.py # Public API exports
12- ├── base.py # Core implementation (~1000 lines)
13- └── event_loop_thread.py # Sync/async bridge for threading
11+ ├── __init__.py # Public API exports
12+ ├── config.py # GCacheKey, GCacheKeyConfig, GCacheConfig, RedisConfig, Serializer
13+ ├── exceptions.py # All exception classes
14+ ├── gcache.py # GCache main class and @cached decorator
15+ └── _internal/ # Implementation details (not public API)
16+ ├── constants.py # Named constants (cache sizes, TTLs, thresholds)
17+ ├── event_loop_thread.py # EventLoopThread, EventLoopThreadPool
18+ ├── local_cache.py # LocalCache (TTLCache-based)
19+ ├── metrics.py # GCacheMetrics (Prometheus)
20+ ├── redis_cache.py # RedisCache
21+ └── wrappers.py # CacheController, CacheChain
22+
1423tests/
15- ├── conftest.py # Fixtures (redis_server, gcache, cache_config_provider)
16- ├── test_gcache.py # Main test suite
17- └── test_*.py # Specialized tests
24+ ├── conftest.py # Fixtures (redis_server, gcache, cache_config_provider)
25+ └── test_*.py # Test suites
1826```
1927
2028## Key Components
@@ -23,73 +31,40 @@ tests/
2331- ** CacheLayer** : Enum - ` NOOP ` , ` LOCAL ` (TTLCache), ` REMOTE ` (Redis)
2432- ** CacheChain** : Chains local → redis with read-through strategy
2533- ** EventLoopThreadPool** : Runs async code from sync cached functions (16 threads)
26- - ** GCacheKey** : URN-formatted cache keys with invalidation tracking
27-
28- ## Core Pattern: @cached Decorator
29-
30- ``` python
31- @gcache.cached (
32- key_type = " user_id" , # Entity type
33- id_arg = " user_id" , # Arg name for cache key
34- use_case = " GetUser" , # Unique identifier
35- arg_adapters = {" request" : lambda r : r.id}, # Complex arg → string
36- ignore_args = [" logger" ], # Args not in cache key
37- )
38- async def get_user (user_id : str , request : Request, logger : Logger) -> User:
39- ...
40- ```
34+ - ** GCacheKey** : Frozen dataclass for cache keys (key_type, id, use_case, args)
4135
4236## Critical Patterns
4337
44- 1 . ** Context-based enable** : Cache is disabled by default
45- ``` python
46- with gcache.enable():
47- result = cached_func() # Actually uses cache
48- ```
38+ 1 . ** Context-based enable** : Cache is disabled by default - use ` with gcache.enable(): `
4939
50- 2 . ** Sync functions use thread pool** : Sync ` @cached ` functions run through ` EventLoopThreadPool ` to avoid blocking
40+ 2 . ** Sync functions use thread pool** : Sync ` @cached ` functions run through ` EventLoopThreadPool `
5141
52- 3 . ** No reentrant sync calls ** : Sync cached function calling another sync cached function raises ` ReentrantSyncFunctionDetected ` - convert to async
42+ 3 . ** Don't call sync cached functions from async ** : Blocks event loop (logs warning)
5343
54- 4 . ** Thread-local Redis clients ** : ` RedisCache ` stores client per-thread via ` threading.local() `
44+ 4 . ** No reentrant sync calls ** : Raises ` ReentrantSyncFunctionDetected ` - convert to async
5545
56- 5 . ** Watermark invalidation ** : Uses timestamps to invalidate without deleting keys
46+ 5 . ** Thread-local Redis clients ** : ` RedisCache ` stores client per-thread via ` threading.local() `
5747
5848## Code Conventions
5949
60- - ** Type hints required** : ` mypy --disallow_untyped_defs `
61- - ** Line length** : 120 chars
62- - ** Linting** : ruff (E4, E7, E9, F, I, UP, ASYNC)
63- - ** Docstrings** : Numpy style
64- - ** Python** : 3.10+ (uses ` | ` union syntax)
50+ - Type hints required, line length 120, ruff + mypy
51+ - Python 3.10+ (uses ` | ` union syntax)
6552
6653## Testing
6754
68- - pytest + pytest-asyncio
69- - ` redislite ` for in-memory Redis
70- - Always test both sync and async paths
71- - Key fixtures: ` gcache ` , ` redis_server ` , ` cache_config_provider `
72-
73- Run tests:
7455``` bash
75- pytest tests/
56+ poetry run pytest tests/
7657```
7758
78- ## When Modifying
79-
80- 1 . ** Update both sync/async paths** - changes typically affect both
81- 2 . ** Preserve context variables** - critical for ` gcache.enable() ` across threads
82- 3 . ** Test both redis_config and redis_client_factory paths**
83- 4 . ** Add Prometheus metrics** for new cache behaviors
84- 5 . ** Run pre-commit** - ruff format, mypy, poetry-check
85-
8659## Common Gotchas
8760
8861- GCache is singleton - second instantiation raises ` GCacheAlreadyInstantiated `
89- - "watermark" is a reserved use_case name
90- - Local cache cannot be invalidated (TTL-only expiration)
91- - Large payloads (>50KB) serialize in executor to avoid blocking
62+ - "watermark" is reserved use_case name
63+ - Local cache cannot be invalidated across instances (TTL-only)
64+ - ` WATERMARK_TTL_SECONDS ` (4 hours) must exceed your longest cache TTL for invalidation to work
65+ - uvloop is optional - falls back to asyncio on Windows/PyPy
9266
9367## Dependencies
9468
95- Core: pydantic, prometheus-client, cachetools, redis, uvloop
69+ Core: pydantic, prometheus-client, cachetools, redis
70+ Optional: uvloop
0 commit comments