Completed Phase 3E-Final with Redis caching upgrade and WebSocket X-Check
integration for real-time defensive play resolution.
## Redis Caching System
### New Files
- app/services/redis_client.py - Async Redis client with connection pooling
* 10 connection pool size
* Automatic connect/disconnect lifecycle
* Ping health checks
* Environment-configurable via REDIS_URL
### Modified Files
- app/services/position_rating_service.py - Migrated from in-memory to Redis
* Redis key pattern: "position_ratings:{card_id}"
* TTL: 86400 seconds (24 hours)
* Graceful fallback if Redis unavailable
* Individual and bulk cache clearing (scan_iter)
* 760x performance improvement (0.274s API → 0.000361s Redis)
- app/main.py - Added Redis startup/shutdown events
* Connect on app startup with settings.redis_url
* Disconnect on shutdown
* Warning logged if Redis connection fails
- app/config.py - Added redis_url setting
* Default: "redis://localhost:6379/0"
* Override via REDIS_URL environment variable
- app/services/__init__.py - Export redis_client
### Testing
- test_redis_cache.py - Live integration test
* 10-step validation: connect, cache miss, cache hit, performance, etc.
* Verified 760x speedup with player 8807 (7 positions)
* Data integrity checks pass
## X-Check WebSocket Integration
### Modified Files
- app/websocket/handlers.py - Enhanced submit_manual_outcome handler
* Serialize XCheckResult to JSON when present
* Include x_check_details in play_resolved broadcast
* Fixed bug: Use result.outcome instead of submitted outcome
* Includes defender ratings, dice rolls, resolution steps
### New Files
- app/websocket/X_CHECK_FRONTEND_GUIDE.md - Comprehensive frontend documentation
* Event structure and field definitions
* Implementation examples (basic, enhanced, polished)
* Error handling and common pitfalls
* Test scenarios with expected data
* League differences (SBA vs PD)
* 500+ lines of frontend integration guide
- app/websocket/MANUAL_VS_AUTO_MODE.md - Workflow documentation
* Manual mode: Players read cards, submit outcomes
* Auto mode: System generates from ratings (PD only)
* X-Check resolution comparison
* UI recommendations for each mode
* Configuration reference
* Testing considerations
### Testing
- tests/integration/test_xcheck_websocket.py - WebSocket integration tests
* Test X-Check play includes x_check_details ✅
* Test non-X-Check plays don't include details ✅
* Full event structure validation
## Performance Impact
- Redis caching: 760x speedup for position ratings
- WebSocket: No performance impact (optional field)
- Graceful degradation: System works without Redis
## Phase 3E-Final Progress
- ✅ WebSocket event handlers for X-Check UI
- ✅ Frontend integration documentation
- ✅ Redis caching upgrade (from in-memory)
- ✅ Redis connection pool in app lifecycle
- ✅ Integration tests (2 WebSocket, 1 Redis)
- ✅ Manual vs Auto mode workflow documentation
Phase 3E-Final: 100% Complete
Phase 3 Overall: ~98% Complete
## Testing Results
All tests passing:
- X-Check table tests: 36/36 ✅
- WebSocket integration: 2/2 ✅
- Redis live test: 10/10 steps ✅
## Configuration
Development:
REDIS_URL=redis://localhost:6379/0 (Docker Compose)
Production options:
REDIS_URL=redis://10.10.0.42:6379/0 (DB server)
REDIS_URL=redis://your-redis-cloud.com:6379/0 (Managed)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
113 lines
2.9 KiB
Python
113 lines
2.9 KiB
Python
"""
|
|
Redis connection client for caching.
|
|
|
|
Provides async Redis client with connection pooling for
|
|
position ratings and other cached data.
|
|
|
|
Author: Claude
|
|
Date: 2025-11-03
|
|
Phase: 3E-Final
|
|
"""
|
|
import logging
|
|
from typing import Optional
|
|
import redis.asyncio as redis
|
|
from redis.asyncio import Redis
|
|
from redis.exceptions import RedisError
|
|
|
|
logger = logging.getLogger(f'{__name__}.RedisClient')
|
|
|
|
|
|
class RedisClient:
|
|
"""
|
|
Async Redis client with connection pooling.
|
|
|
|
Singleton pattern - use redis_client instance.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._redis: Optional[Redis] = None
|
|
self._url: Optional[str] = None
|
|
|
|
async def connect(self, redis_url: str = "redis://localhost:6379/0") -> None:
|
|
"""
|
|
Connect to Redis server.
|
|
|
|
Args:
|
|
redis_url: Redis connection URL
|
|
Format: redis://host:port/db
|
|
Example: redis://localhost:6379/0
|
|
"""
|
|
if self._redis is not None:
|
|
logger.warning("Redis client already connected")
|
|
return
|
|
|
|
try:
|
|
self._url = redis_url
|
|
self._redis = await redis.from_url(
|
|
redis_url,
|
|
encoding="utf-8",
|
|
decode_responses=True,
|
|
max_connections=10 # Connection pool size
|
|
)
|
|
|
|
# Test connection
|
|
await self._redis.ping()
|
|
logger.info(f"Redis connected successfully: {redis_url}")
|
|
|
|
except RedisError as e:
|
|
logger.error(f"Failed to connect to Redis at {redis_url}: {e}")
|
|
self._redis = None
|
|
raise
|
|
|
|
async def disconnect(self) -> None:
|
|
"""Disconnect from Redis server."""
|
|
if self._redis is not None:
|
|
await self._redis.close()
|
|
await self._redis.connection_pool.disconnect()
|
|
self._redis = None
|
|
logger.info("Redis disconnected")
|
|
|
|
@property
|
|
def client(self) -> Redis:
|
|
"""
|
|
Get Redis client instance.
|
|
|
|
Returns:
|
|
Redis client
|
|
|
|
Raises:
|
|
RuntimeError: If not connected
|
|
"""
|
|
if self._redis is None:
|
|
raise RuntimeError(
|
|
"Redis client not connected. "
|
|
"Call await redis_client.connect() first."
|
|
)
|
|
return self._redis
|
|
|
|
@property
|
|
def is_connected(self) -> bool:
|
|
"""Check if Redis is connected."""
|
|
return self._redis is not None
|
|
|
|
async def ping(self) -> bool:
|
|
"""
|
|
Test Redis connection.
|
|
|
|
Returns:
|
|
True if Redis is responding, False otherwise
|
|
"""
|
|
if not self.is_connected:
|
|
return False
|
|
|
|
try:
|
|
await self._redis.ping()
|
|
return True
|
|
except RedisError as e:
|
|
logger.error(f"Redis ping failed: {e}")
|
|
return False
|
|
|
|
|
|
# Singleton instance
|
|
redis_client = RedisClient()
|