feat: add redis cache for copyright label lookups (#566)
* feat: add redis cache for copyright label lookups
the moderation service call was causing 2-3s latency spikes on GET /tracks/
in production. since we have multiple fly.io instances, in-memory caching
wouldn't work - added distributed redis cache (reusing docket's redis).
- add backend/utilities/redis.py for async client from docket URL
- cache active label status with 5min TTL (matches queue cache)
- use mget/pipeline for efficient batch operations
- invalidate cache when labels are emitted
- fail closed on errors (treat as active)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move cache settings to config, add async context manager
- add label_cache_prefix and label_cache_ttl_seconds to ModerationSettings
- add async_redis_client() context manager for isolated connections
- add clear_client_cache() for test cleanup
- remove hardcoded cache constants from moderation.py
- fix tests to properly clear client cache between runs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test: add redis isolation for xdist parallel tests
- add redis_database fixture that assigns different DB per worker
- use unique URIs in cache tests to avoid cross-test pollution
- document redis test isolation pattern in docs/testing/README.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: handle missing redis gracefully in test fixture
the redis_database fixture now catches ConnectionError and skips
silently when redis is unavailable. tests that don't need redis
will pass, and tests that do need it will fail with specific errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: mock settings in moderation cache tests
tests that mock httpx must also mock settings.moderation to avoid
early return on auth_token check. without this, tests pass locally
(where MODERATION_AUTH_TOKEN may be set) but fail in CI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use D: prefix for DOCKET_URL to allow CI override
pytest-env's D: prefix means "default" - only set if not already
set by the environment. this allows CI's DOCKET_URL=redis://localhost:6379
to take precedence over pyproject.toml's 6380 (for local docker-compose).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: move deferred imports to top of file, add type hints
- move redis imports to module level in moderation.py
- move asyncio import to module level in redis.py
- add type hint for kwargs dict in redis.py
- move imports to module level in conftest.py
- add rule to AGENTS.md: DO NOT UNNECESSARILY DEFER IMPORTS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
authored by