strat-gameplay-webapp/backend/app/services/redis_client.py
Cal Corum adf7c7646d CLAUDE: Phase 3E-Final - Redis Caching & X-Check WebSocket Integration
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>
2025-11-03 22:46:59 -06:00

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()