Skip to content

Commit 9d4f9ce

Browse files
authored
chore: update README.md and AGENTS.md (#102)
1 parent 3784261 commit 9d4f9ce

2 files changed

Lines changed: 343 additions & 225 deletions

File tree

AGENTS.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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

Comments
 (0)