|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +## Project Overview |
| 4 | + |
| 5 | +gcache is a fine-grained caching library with multi-layer support (local + Redis) and a decorator-based API. It works with both sync and async functions. |
| 6 | + |
| 7 | +## Structure |
| 8 | + |
| 9 | +``` |
| 10 | +src/gcache/ |
| 11 | +├── __init__.py # Public API exports |
| 12 | +├── base.py # Core implementation (~1000 lines) |
| 13 | +└── event_loop_thread.py # Sync/async bridge for threading |
| 14 | +tests/ |
| 15 | +├── conftest.py # Fixtures (redis_server, gcache, cache_config_provider) |
| 16 | +├── test_gcache.py # Main test suite |
| 17 | +└── test_*.py # Specialized tests |
| 18 | +``` |
| 19 | + |
| 20 | +## Key Components |
| 21 | + |
| 22 | +- **GCache**: Singleton main class, provides `@cached` decorator |
| 23 | +- **CacheLayer**: Enum - `NOOP`, `LOCAL` (TTLCache), `REMOTE` (Redis) |
| 24 | +- **CacheChain**: Chains local → redis with read-through strategy |
| 25 | +- **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 | +``` |
| 41 | + |
| 42 | +## Critical Patterns |
| 43 | + |
| 44 | +1. **Context-based enable**: Cache is disabled by default |
| 45 | + ```python |
| 46 | + with gcache.enable(): |
| 47 | + result = cached_func() # Actually uses cache |
| 48 | + ``` |
| 49 | + |
| 50 | +2. **Sync functions use thread pool**: Sync `@cached` functions run through `EventLoopThreadPool` to avoid blocking |
| 51 | + |
| 52 | +3. **No reentrant sync calls**: Sync cached function calling another sync cached function raises `ReentrantSyncFunctionDetected` - convert to async |
| 53 | + |
| 54 | +4. **Thread-local Redis clients**: `RedisCache` stores client per-thread via `threading.local()` |
| 55 | + |
| 56 | +5. **Watermark invalidation**: Uses timestamps to invalidate without deleting keys |
| 57 | + |
| 58 | +## Code Conventions |
| 59 | + |
| 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) |
| 65 | + |
| 66 | +## Testing |
| 67 | + |
| 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: |
| 74 | +```bash |
| 75 | +pytest tests/ |
| 76 | +``` |
| 77 | + |
| 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 | + |
| 86 | +## Common Gotchas |
| 87 | + |
| 88 | +- 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 |
| 92 | + |
| 93 | +## Dependencies |
| 94 | + |
| 95 | +Core: pydantic, prometheus-client, cachetools, redis, uvloop |
0 commit comments