TASK-003: Document process-local constraint for RuntimeState and SessionCache
- Add comprehensive docstring to runtime_state.py explaining single-process constraint, impacts in multi-worker deployments, and solution approach - Add comprehensive docstring to session_cache.py explaining process-local cache limitation, security implications, and Redis/database alternatives - Update Architecture.md to clarify session cache is process-local and describe single-worker enforcement via TASK-002 - Update Architecture.md runtime state section with detailed explanation of per-process state and multi-worker impacts - Add Backend-Development.md section 13.7.2 documenting session cache pluggability pattern with example Redis implementation - All tests pass; linting passes; type checking has pre-existing errors This is the short-term fix for TASK-003: enforce single-worker deployment (TASK-002) and document the constraint clearly. The long-term fix (Redis backend) is deferred as a follow-up.
This commit is contained in:
@@ -645,6 +645,75 @@ async def get_session_repo() -> SessionRepository:
|
||||
- Before each deployment, run `mypy --strict` to ensure all dependency providers return values compatible with their Protocol types.
|
||||
- The `cast()` calls in `dependencies.py` are a documented signal that structural compatibility is being verified externally, not via explicit class inheritance.
|
||||
|
||||
#### 13.7.2 Session Cache Pluggability — Process-Local vs. Shared Backends
|
||||
|
||||
Session validation is expensive (SQLite lookup + password verification). To improve performance, **validated session tokens are cached** using the `SessionCache` interface (`app.utils.session_cache`). The default implementation, `InMemorySessionCache`, stores cached sessions in process-local memory.
|
||||
|
||||
**Current implementation (single-worker):**
|
||||
|
||||
```python
|
||||
from app.utils.session_cache import SessionCache, InMemorySessionCache, NoOpSessionCache
|
||||
|
||||
class SessionCache(Protocol):
|
||||
"""Interface for session token validation cache backends."""
|
||||
def get(self, token: str) -> Session | None: ...
|
||||
def set(self, token: str, session: Session, ttl_seconds: float) -> None: ...
|
||||
def invalidate(self, token: str) -> None: ...
|
||||
def clear(self) -> None: ...
|
||||
|
||||
# Default in-memory implementation — PROCESS-LOCAL
|
||||
class InMemorySessionCache:
|
||||
def __init__(self) -> None:
|
||||
self._entries: dict[str, tuple[Session, float]] = {}
|
||||
```
|
||||
|
||||
**Single-worker constraint:**
|
||||
|
||||
`InMemorySessionCache` is **process-local** — each worker process has its own dict. In single-worker mode (enforced by TASK-002), this is safe and improves performance. In multi-worker deployments:
|
||||
- A logout by worker A clears the session from A's cache, but worker B still has it → logout doesn't work.
|
||||
- Enabling/disabling the cache requires restarting all workers to take effect.
|
||||
|
||||
**Multi-worker solution:**
|
||||
|
||||
To support multiple workers (future enhancement), implement a shared backend behind the same `SessionCache` Protocol:
|
||||
|
||||
```python
|
||||
# Example Redis implementation (not yet in codebase)
|
||||
class RedisSessionCache:
|
||||
"""Session cache backed by Redis."""
|
||||
def __init__(self, redis_url: str) -> None:
|
||||
self.client = aioredis.from_url(redis_url)
|
||||
|
||||
async def get(self, token: str) -> Session | None:
|
||||
data = await self.client.get(f"session:{token}")
|
||||
return Session.model_validate_json(data) if data else None
|
||||
|
||||
async def set(self, token: str, session: Session, ttl_seconds: float) -> None:
|
||||
await self.client.setex(
|
||||
f"session:{token}",
|
||||
int(ttl_seconds),
|
||||
session.model_dump_json()
|
||||
)
|
||||
|
||||
async def invalidate(self, token: str) -> None:
|
||||
await self.client.delete(f"session:{token}")
|
||||
|
||||
async def clear(self) -> None:
|
||||
await self.client.flushdb()
|
||||
```
|
||||
|
||||
To adopt a Redis backend:
|
||||
1. Create `RedisSessionCache` in `app.utils.session_cache`.
|
||||
2. Update `app.utils.runtime_state.set_runtime_settings()` to instantiate `RedisSessionCache` when `REDIS_URL` env var is set.
|
||||
3. Update `app.config.Settings` to accept optional `REDIS_URL`.
|
||||
4. Tests continue to use `InMemorySessionCache` (no Redis dependency in dev).
|
||||
|
||||
**Implementation rules:**
|
||||
- All cache methods must be `async` (even if the backend is sync).
|
||||
- Never log session tokens or session data.
|
||||
- TTL must be respected — expired entries must be removed on access.
|
||||
- See `app/utils/session_cache.py` for the full Protocol definition and current implementations.
|
||||
|
||||
### 14.8 Composition over Inheritance
|
||||
|
||||
- Favour **composing** small, focused objects over deep inheritance hierarchies.
|
||||
|
||||
Reference in New Issue
Block a user