Files
roa2web-service-auto/shared/docs/REDIS_IMPLEMENTATION_PLAN.md
Marius Mutu 9008876b16 chore: Remove obsolete microservices directories and update all references
- Delete data-entry-app/ (1.6GB), reports-app/ (447MB), .auto-build-data/
- Saved ~1.4GB disk space (64% reduction: 2.2GB → 845MB)

Updated references across 38 files:
- .claude/rules/ paths: backend/modules/, src/modules/
- .claude/commands/validate.md: all validation paths
- docs/ (13 files): data-entry, telegram, README, CLAUDE.md
- scripts/ (3 files): backup-secrets, restore-secrets, test-docker
- security/ (2 files): git_cleanup, SECURITY_PROCEDURES
- deployment/ & shared/: updated all stale comments

All paths now reflect ultrathin monolith architecture:
- Backend: backend/modules/{reports,data_entry,telegram}/
- Frontend: src/modules/{reports,data-entry}/
- Shared: shared/{auth,database,routes}/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 12:08:20 +02:00

911 lines
30 KiB
Markdown

# Plan Implementare Redis Caching - ROA2WEB
**Data Creare:** 2025-01-25
**Status:** DRAFT - Ready for Implementation
**Durata Estimată:** 2-3 ore
---
## 📋 Sumar Executiv
- **Obiectiv:** Implementare Redis caching layer pentru reducerea query-urilor repetitive la Oracle DB
- **Target:** 60-80% reducere în numărul de query-uri pentru date frecvent accesate
- **Strategie:** Caching simplu la nivel de Service layer cu invalidation manuală
- **Backward Compatibility:** ✅ Aplicația funcționează fără Redis (graceful degradation)
- **Multi-Tenant Ready:** ✅ Cache keys pregătite pentru viitoare multi-tenancy
**Infrastructure Status:**
- ✅ Redis container configurat în `docker-compose.yml` (lines 147-163)
- ✅ Backend `depends_on: roa-redis`
- ❌ Redis client library LIPSĂ
- ❌ Cod de caching LIPSĂ
---
## 🗂️ Structura Fișierelor
### Fișiere Noi (6 fișiere)
- [ ] `shared/cache/__init__.py` - Package initialization
- [ ] `shared/cache/redis_client.py` - Redis connection client (90 lines)
- [ ] `shared/cache/decorators.py` - Cache decorators (60 lines)
- [ ] `shared/cache/utils.py` - Cache key generators, helpers (40 lines)
- [ ] `shared/tests/test_redis_client.py` - Unit tests pentru Redis client (120 lines)
- [ ] `shared/tests/test_cache_decorators.py` - Unit tests pentru decorators (80 lines)
### Fișiere Modificate (7 fișiere)
- [ ] `backend/requirements.txt` - Adaugă redis>=5.0.0
- [ ] `backend/.env.example` - Adaugă REDIS_* env vars
- [ ] `backend/app/main.py` - Initialize Redis at startup
- [ ] `backend/app/services/dashboard_service.py` - Apply caching
- [ ] `backend/app/services/invoice_service.py` - Apply caching
- [ ] `backend/app/routers/dashboard.py` - Cache invalidation on mutations
- [ ] `backend/app/routers/invoices.py` - Cache invalidation on mutations
---
## 🚀 Faze de Implementare
### FAZA 1: Setup Redis Client și Connection (30 min)
**Obiectiv:** Creează Redis client singleton cu connection pooling, similar cu `OraclePool` pattern.
**Tasks:**
1. [ ] **Adaugă dependency în requirements.txt**
- Fișier: `backend/requirements.txt`
- Acțiune: Adaugă linia `redis>=5.0.0` după `httpx>=0.27.0`
- Motivație: redis 5.0+ are async support nativ
2. [ ] **Creează package cache în shared/**
- Fișiere: `shared/cache/__init__.py`
- Acțiune: Creează directorul și fișierul de init cu exports:
```python
from .redis_client import redis_cache
from .decorators import cached, invalidate_cache
__all__ = ['redis_cache', 'cached', 'invalidate_cache']
```
3. [ ] **Implementează RedisCache singleton client**
- Fișier: `shared/cache/redis_client.py`
- Acțiune: Creează clasa `RedisCache` similar cu `OraclePool`:
- Singleton pattern (nu multi-instance)
- Async redis client cu connection pooling
- Methods: `initialize()`, `get()`, `set()`, `delete()`, `delete_pattern()`, `close()`
- Graceful degradation: dacă Redis e down, log warning și return None
- Connection retry cu exponential backoff
- Template (vezi secțiunea Code Templates mai jos)
4. [ ] **Adaugă env variables în .env.example**
- Fișier: `backend/.env.example`
- Acțiune: Adaugă la sfârșitul fișierului:
```bash
# Redis Configuration
REDIS_URL=redis://roa-redis:6379/0
REDIS_PASSWORD=roa2web_redis_password
REDIS_ENABLED=true
CACHE_DEFAULT_TTL=300
```
5. [ ] **Initialize Redis la startup în main.py**
- Fișier: `backend/app/main.py`
- Acțiune: În funcția `startup_event()`:
```python
from shared.cache import redis_cache
@app.on_event("startup")
async def startup_event():
# ... existing oracle pool init ...
# Initialize Redis cache
if os.getenv('REDIS_ENABLED', 'false').lower() == 'true':
await redis_cache.initialize(
url=os.getenv('REDIS_URL'),
password=os.getenv('REDIS_PASSWORD')
)
@app.on_event("shutdown")
async def shutdown_event():
# ... existing oracle pool close ...
await redis_cache.close()
```
**Output Verificabil:**
- [ ] `pip install -r requirements.txt` rulează fără erori
- [ ] Redis client se conectează cu succes la container
- [ ] Test manual: `python -c "import asyncio; from shared.cache import redis_cache; asyncio.run(redis_cache.initialize())"`
- [ ] Log message: "✅ Redis cache initialized successfully"
---
### FAZA 2: Cache Decorator și Helpers (30 min)
**Obiectiv:** Creează decorator `@cached()` pentru aplicare ușoară în Services.
**Tasks:**
1. [ ] **Implementează cache key generator**
- Fișier: `shared/cache/utils.py`
- Acțiune: Funcții helper pentru key generation:
```python
import hashlib
import json
from typing import Any, Dict
def make_cache_key(tenant_id: str, resource: str, **params) -> str:
"""
Generate tenant-aware cache key
Format: cache:{tenant_id}:{resource}:{params_hash}
"""
params_str = json.dumps(params, sort_keys=True)
params_hash = hashlib.md5(params_str.encode()).hexdigest()[:12]
return f"cache:{tenant_id}:{resource}:{params_hash}"
def extract_tenant_id(kwargs: Dict[str, Any]) -> str:
"""
Extract tenant_id from function kwargs
For now returns 'default', later extract from JWT token
"""
# TODO: Extract from request.state.tenant_id when multi-tenant implemented
return kwargs.get('tenant_id', 'default')
```
2. [ ] **Implementează @cached decorator**
- Fișier: `shared/cache/decorators.py`
- Acțiune: Decorator pentru auto-caching de funcții async:
```python
from functools import wraps
from typing import Callable, Optional
import logging
from .redis_client import redis_cache
from .utils import make_cache_key, extract_tenant_id
logger = logging.getLogger(__name__)
def cached(resource: str, ttl: int = 300):
"""
Cache decorator pentru funcții async
Usage:
@cached(resource="dashboard_summary", ttl=300)
async def get_dashboard_summary(company: str, username: str):
# ... query Oracle ...
return data
Args:
resource: Resource name (e.g., 'dashboard_summary', 'invoices_list')
ttl: Time-to-live în secunde (default: 5 min)
"""
def decorator(func: Callable):
@wraps(func)
async def wrapper(*args, **kwargs):
# Skip cache dacă Redis e disabled
if not redis_cache.is_enabled():
return await func(*args, **kwargs)
# Extract tenant_id și params pentru cache key
tenant_id = extract_tenant_id(kwargs)
cache_params = {k: v for k, v in kwargs.items()
if k not in ['username', 'current_user']}
cache_key = make_cache_key(tenant_id, resource, **cache_params)
# Try cache GET
cached_value = await redis_cache.get(cache_key)
if cached_value is not None:
logger.debug(f"Cache HIT: {cache_key}")
return cached_value
# Cache MISS - execute function
logger.debug(f"Cache MISS: {cache_key}")
result = await func(*args, **kwargs)
# Save to cache
await redis_cache.set(cache_key, result, ttl=ttl)
return result
return wrapper
return decorator
```
3. [ ] **Implementează invalidate_cache helper**
- Fișier: `shared/cache/decorators.py` (same file)
- Acțiune: Helper function pentru manual invalidation:
```python
async def invalidate_cache(
tenant_id: str = "default",
resource: Optional[str] = None
):
"""
Invalidate cache entries
Examples:
await invalidate_cache(resource="dashboard_summary") # clear specific resource
await invalidate_cache() # clear all for default tenant
"""
if not redis_cache.is_enabled():
return
if resource:
pattern = f"cache:{tenant_id}:{resource}:*"
else:
pattern = f"cache:{tenant_id}:*"
await redis_cache.delete_pattern(pattern)
logger.info(f"Cache invalidated: {pattern}")
```
**Output Verificabil:**
- [ ] Decorator funcționează fără erori
- [ ] Cache key format: `cache:default:dashboard_summary:abc123`
- [ ] Test unit: `pytest shared/tests/test_cache_decorators.py -v`
---
### FAZA 3: Aplicare în Endpoint-uri (45 min)
**Obiectiv:** Aplică caching în Service layer pentru dashboard și invoices.
**Tasks:**
1. [ ] **Apply @cached în DashboardService.get_complete_summary()**
- Fișier: `backend/app/services/dashboard_service.py`
- Acțiune: Adaugă decorator la metoda `get_complete_summary`:
```python
from shared.cache import cached
class DashboardService:
@staticmethod
@cached(resource="dashboard_summary", ttl=300) # 5 min
async def get_complete_summary(company: str, username: str):
# ... existing implementation ...
```
- Motivație: Dashboard e accesat des, datele se schimbă rar
2. [ ] **Apply @cached în DashboardService.get_trends()**
- Fișier: `backend/app/services/dashboard_service.py`
- Acțiune: Similar, TTL=180 (3 min pentru trends)
- Cache key va include period: `cache:default:dashboard_trends:company-X:period-30d:abc123`
3. [ ] **Apply @cached în DashboardService.get_detailed_data()**
- Fișier: `backend/app/services/dashboard_service.py`
- Acțiune: TTL=60 (1 min pentru tabel detalii - se refreshează des)
- Cache key include page, page_size, search
4. [ ] **Apply @cached în InvoiceService.get_invoices()**
- Fișier: `backend/app/services/invoice_service.py`
- Acțiune: TTL=60 (1 min)
- Cache key include filter params (partner_type, date_from, date_to, etc.)
5. [ ] **Apply @cached în InvoiceService.get_invoice_summary()**
- Fișier: `backend/app/services/invoice_service.py`
- Acțiune: TTL=180 (3 min pentru summary)
6. [ ] **Cache invalidation în dashboard mutations (viitor)**
- Fișier: `backend/app/routers/dashboard.py`
- Acțiune: Pregătește cod pentru invalidation (de activat când există POST/PUT/DELETE):
```python
# TODO: Activate când implementăm mutations
# from shared.cache import invalidate_cache
#
# @router.post("/...")
# async def update_dashboard_data(...):
# # ... update logic ...
# await invalidate_cache(resource="dashboard_summary")
# await invalidate_cache(resource="dashboard_trends")
```
7. [ ] **Cache invalidation în invoice mutations**
- Fișier: `backend/app/routers/invoices.py`
- Acțiune: Când se implementează POST/PUT/DELETE pentru invoices, invalidează:
- `invoices_list`
- `invoices_summary`
- `dashboard_summary` (afectează dashboard)
**Output Verificabil:**
- [ ] Backend pornește fără erori
- [ ] First request: Cache MISS + Oracle query (măsoară timp: ~500-1000ms)
- [ ] Second request (same params): Cache HIT (măsoară timp: ~10-20ms)
- [ ] Cache hit rate > 80% după 100 requests repetitive
- [ ] Logs arată `Cache HIT/MISS` messages
---
### FAZA 4: Testing, Monitoring și Cleanup (45 min)
**Obiectiv:** Validare funcționare corectă, performance benchmarks, și documentare.
**Tasks:**
1. [ ] **Unit tests pentru RedisCache client**
- Fișier: `shared/tests/test_redis_client.py`
- Acțiune: Testează:
- Connection success/failure
- Get/Set/Delete operations
- Pattern matching delete
- Graceful degradation când Redis e down
- Run: `pytest shared/tests/test_redis_client.py -v`
2. [ ] **Unit tests pentru cache decorators**
- Fișier: `shared/tests/test_cache_decorators.py`
- Acțiune: Testează:
- Decorator aplică caching corect
- Cache key generation
- TTL respectat
- Invalidation funcționează
- Run: `pytest shared/tests/test_cache_decorators.py -v`
3. [ ] **Integration test în Docker**
- Acțiune: Pornește stack complet cu `docker-compose up`
- Verifică:
- Backend se conectează la Redis
- Cache funcționează end-to-end
- Logs arată cache hits/misses
4. [ ] **Performance benchmark**
- Tool: Apache Bench sau Python requests loop
- Test case: 100 requests la `/api/dashboard/summary?company=X`
- Măsoară:
- **Without cache** (REDIS_ENABLED=false):
- Avg response time: ~800ms
- Total time: ~80 seconds
- **With cache** (REDIS_ENABLED=true):
- First request: ~800ms (MISS)
- Next 99 requests: ~15ms (HIT)
- Total time: ~2 seconds
- **Improvement: 97.5%**
- Salvează results în `shared/docs/REDIS_PERFORMANCE_BENCHMARK.md`
5. [ ] **Manual testing checklist**
- [ ] Dashboard: Refresh multiple ori (verify cache HIT în logs)
- [ ] Invoices: Filtrare diferită (verify cache keys unice)
- [ ] Redis failure test: Stop Redis container, verify app funcționează (fallback la Oracle)
- [ ] Cache invalidation: Manual invalidate via Redis CLI, verify re-query
6. [ ] **Update CLAUDE.md documentation**
- Fișier: `CLAUDE.md`
- Acțiune: Adaugă secțiune "Redis Caching":
```markdown
## 💾 Redis Caching
ROA2WEB folosește Redis pentru caching layer:
- **Client**: `shared/cache/redis_client.py` (singleton pattern)
- **Decorator**: `@cached(resource="name", ttl=300)` în Services
- **Cache Keys**: `cache:{tenant_id}:{resource}:{params_hash}`
- **TTL Defaults**:
- Dashboard summary: 5 min
- Dashboard trends: 3 min
- Invoices list: 1 min
- Invoices summary: 3 min
**Toggle cache:** Set `REDIS_ENABLED=false` în `.env`
**Invalidate manual:**
```python
from shared.cache import invalidate_cache
await invalidate_cache(resource="dashboard_summary")
```
**Performance:** 60-80% reduction în query time pentru repetitive requests
```
**Output Verificabil:**
- [ ] All tests pass: `pytest shared/tests/ -v`
- [ ] Performance benchmark shows >60% improvement
- [ ] Manual testing checklist complet
- [ ] Documentation updated
- [ ] Ready for code review
---
## 📊 Cache Strategy
### Resource TTLs
| Resource | TTL | Motivație |
|----------|-----|-----------|
| `dashboard_summary` | 300s (5 min) | Date agregate, se schimbă rar |
| `dashboard_trends` | 180s (3 min) | Trends calculation expensive |
| `dashboard_detailed_data` | 60s (1 min) | Tabel interactiv, refresh frecvent |
| `dashboard_performance` | 180s (3 min) | Performance metrics stabile |
| `dashboard_cashflow` | 180s (3 min) | Forecast calculation expensive |
| `dashboard_maturity` | 180s (3 min) | Maturity analysis complex |
| `invoices_list` | 60s (1 min) | Listing cu filtre, refresh frecvent |
| `invoices_summary` | 180s (3 min) | Summary stats stabile |
| `companies_list` | 600s (10 min) | Lista rareori se schimbă |
| `treasury_data` | 120s (2 min) | Trezorerie moderate changes |
**Raționament TTL:**
- Scurt (60s): Date interactive, tabel listings
- Mediu (180-300s): Calculații expensive, agregări
- Lung (600s+): Date aproape statice (companies, permissions)
### Cache Keys Pattern
**Format:** `cache:{tenant_id}:{resource}:{params_hash}`
**Exemplu concret:**
```
cache:default:dashboard_summary:company-123:abc456def789
cache:default:invoices_list:company-123:partner-CLIENTI:unpaid-true:xyz890
cache:default:dashboard_trends:company-456:period-30d:compare-true:def123
```
**Componente:**
- `cache:` - Prefix constant (pentru separare de alte Redis keys)
- `{tenant_id}` - Tenant ID (deocamdată "default", viitor: din JWT token)
- `{resource}` - Resource name (dashboard_summary, invoices_list, etc.)
- `{params_hash}` - MD5 hash (primele 12 caractere) al parametrilor sortați JSON
**Multi-Tenant Ready:**
Când se implementează multi-tenant:
1. Modifică `extract_tenant_id()` în `utils.py` să citească din `request.state.tenant_id`
2. JWT token va include `tenant_id` field
3. Cache keys automat vor fi per-tenant
4. Invalidation per-tenant: `await invalidate_cache(tenant_id="client-a")`
### Invalidation Rules
**Trigger:** Când se schimbă date în Oracle DB
| Mutation | Invalidate Resources |
|----------|---------------------|
| Invoice created/updated | `invoices_list`, `invoices_summary`, `dashboard_summary`, `dashboard_trends` |
| Payment recorded | `invoices_list`, `dashboard_summary`, `treasury_data`, `dashboard_cashflow` |
| Treasury transaction | `treasury_data`, `dashboard_summary`, `dashboard_cashflow` |
| Company settings changed | `companies_list`, `dashboard_*` (pentru acea companie) |
**Implementare:**
```python
# În router după mutation
from shared.cache import invalidate_cache
@router.post("/invoices/{invoice_id}/pay")
async def mark_invoice_paid(...):
# ... update DB ...
# Invalidate affected caches
await invalidate_cache(resource="invoices_list")
await invalidate_cache(resource="invoices_summary")
await invalidate_cache(resource="dashboard_summary")
await invalidate_cache(resource="treasury_data")
return {"status": "ok"}
```
**Pattern Matching:**
```python
# Invalidate toate cache-urile pentru dashboard
await invalidate_cache(resource="dashboard") # matches dashboard_*
# Invalidate tot pentru un tenant
await invalidate_cache(tenant_id="client-a") # matches cache:client-a:*
```
---
## 🧪 Testing Plan
### Unit Tests
**File:** `shared/tests/test_redis_client.py`
- [ ] `test_redis_connection_success()` - Verify successful connection
- [ ] `test_redis_connection_failure_graceful()` - Redis down, no exception thrown
- [ ] `test_redis_get_set_delete()` - Basic operations
- [ ] `test_redis_delete_pattern()` - Pattern matching deletion
- [ ] `test_redis_ttl_expiration()` - Verify TTL works
- [ ] `test_redis_connection_retry()` - Exponential backoff retry
**File:** `shared/tests/test_cache_decorators.py`
- [ ] `test_cached_decorator_hit()` - Second call returns cached value
- [ ] `test_cached_decorator_miss()` - First call queries function
- [ ] `test_cache_key_generation()` - Keys format correct
- [ ] `test_cache_key_unique_params()` - Different params = different keys
- [ ] `test_invalidate_cache_pattern()` - Invalidation works
- [ ] `test_cached_disabled()` - Works when REDIS_ENABLED=false
### Integration Tests
**Setup:** `docker-compose up -d`
**Test Scenarios:**
1. **Full Stack Test:**
- Start backend + Redis
- Call `/api/dashboard/summary?company=123`
- Verify: Oracle query executed (check logs)
- Call again same endpoint
- Verify: Cache hit (no Oracle query)
2. **Cache Invalidation Test:**
- Call endpoint (cache populated)
- Invalidate via `redis-cli KEYS "cache:*"` + `DEL`
- Call endpoint again
- Verify: Oracle query executed (cache miss)
3. **Redis Failure Test:**
- `docker-compose stop roa-redis`
- Call endpoint
- Verify: App works (fallback to Oracle)
- No error thrown
- Logs show warning: "Redis unavailable, fallback to DB"
### Performance Benchmarks
**Tool:** Apache Bench or Python script
**Baseline (No Cache):**
```bash
# Stop Redis or set REDIS_ENABLED=false
ab -n 100 -c 10 http://localhost:8001/api/dashboard/summary?company=123
# Expected: ~800ms avg response time, 80s total
```
**With Cache:**
```bash
# Start Redis and set REDIS_ENABLED=true
ab -n 100 -c 10 http://localhost:8001/api/dashboard/summary?company=123
# Expected: ~15ms avg (after first request), ~2s total
```
**Target Metrics:**
- Cache hit rate: >90% (după warmup)
- Avg response time reduction: >60%
- Total time reduction: >75%
- Memory usage: +50-200MB (Redis)
**Save Results:** `shared/docs/REDIS_PERFORMANCE_BENCHMARK.md`
### Manual Testing Checklist
- [ ] **Dashboard Summary:**
- [ ] First load → check logs for "Cache MISS"
- [ ] Refresh page → check logs for "Cache HIT"
- [ ] Change company → new cache key, "Cache MISS"
- [ ] **Invoices List:**
- [ ] Filter: toate facturile → "Cache MISS" first time
- [ ] Refresh → "Cache HIT"
- [ ] Filter: doar neplatite → new key, "Cache MISS"
- [ ] Refresh → "Cache HIT"
- [ ] **Cache Invalidation:**
- [ ] Load dashboard (cached)
- [ ] Redis CLI: `redis-cli KEYS "cache:*"` → see keys
- [ ] Delete: `redis-cli DEL cache:default:dashboard_summary:*`
- [ ] Refresh dashboard → "Cache MISS" (re-queries Oracle)
- [ ] **Redis Failure Graceful:**
- [ ] Stop Redis: `docker-compose stop roa-redis`
- [ ] Access dashboard → works (no crash)
- [ ] Check logs: "Redis unavailable, using direct DB query"
- [ ] Start Redis: `docker-compose start roa-redis`
- [ ] Access dashboard → caching resume
- [ ] **Multi-Tenant Simulation:**
- [ ] Load dashboard company=123 (tenant=default)
- [ ] Load dashboard company=456 (tenant=default)
- [ ] Verify different cache keys in Redis
---
## 🔧 Configurare Env Variables
**File:** `backend/.env`
```bash
# ============================================================================
# REDIS CONFIGURATION
# ============================================================================
# Redis Connection URL
# Development: redis://roa-redis:6379/0 (Docker network)
# Production: redis://redis-host:6379/0 or redis://localhost:6379/0
REDIS_URL=redis://roa-redis:6379/0
# Redis Password (from docker-compose secrets)
# Match with REDIS_PASSWORD in docker-compose.yml
REDIS_PASSWORD=roa2web_redis_password
# Enable/Disable Redis Caching
# Set to 'false' to disable caching (fallback to direct DB queries)
REDIS_ENABLED=true
# Default Cache TTL (seconds)
# Used when no specific TTL provided to @cached decorator
CACHE_DEFAULT_TTL=300
# Redis Connection Pool Settings (optional, defaults shown)
REDIS_MAX_CONNECTIONS=50
REDIS_SOCKET_CONNECT_TIMEOUT=5
REDIS_SOCKET_KEEPALIVE=true
```
**Docker Compose Integration:**
No changes needed! Redis container already configured in `docker-compose.yml:147-163`.
**Verify:**
```bash
docker-compose exec roa-backend env | grep REDIS
# Should show REDIS_URL, REDIS_PASSWORD, REDIS_ENABLED
```
---
## 📝 Checklist Final
### Pre-Implementation
- [ ] Read și înțeles planul complet
- [ ] Backup codebase: `git commit -am "Backup before Redis implementation"`
- [ ] Redis container rulează: `docker-compose up -d roa-redis`
- [ ] Test connection: `docker-compose exec roa-redis redis-cli ping` → PONG
### Faza 1 (Setup)
- [ ] Dependency added: `redis>=5.0.0` în requirements.txt
- [ ] Package created: `shared/cache/__init__.py`
- [ ] Redis client: `shared/cache/redis_client.py`
- [ ] Env vars added: `backend/.env.example`
- [ ] Main.py updated: Redis initialize at startup
- [ ] Test: `python -c "import asyncio; from shared.cache import redis_cache; asyncio.run(redis_cache.initialize())"`
### Faza 2 (Decorators)
- [ ] Utils created: `shared/cache/utils.py`
- [ ] Decorator created: `shared/cache/decorators.py`
- [ ] Unit tests: `shared/tests/test_cache_decorators.py`
- [ ] Test: `pytest shared/tests/test_cache_decorators.py -v`
### Faza 3 (Integration)
- [ ] Cached applied: DashboardService.get_complete_summary
- [ ] Cached applied: DashboardService.get_trends
- [ ] Cached applied: DashboardService.get_detailed_data
- [ ] Cached applied: InvoiceService.get_invoices
- [ ] Cached applied: InvoiceService.get_invoice_summary
- [ ] Backend starts: `uvicorn app.main:app --reload`
- [ ] Test: First request slow, second fast
### Faza 4 (Validation)
- [ ] Unit tests pass: `pytest shared/tests/ -v`
- [ ] Integration tests pass (Docker stack)
- [ ] Performance benchmark run (save results)
- [ ] Manual testing checklist completed
- [ ] Documentation updated: `CLAUDE.md`
- [ ] Git commit: `git add . && git commit -m "feat: implement Redis caching layer"`
### Ready for Production
- [ ] All tests green
- [ ] Performance improvement >60%
- [ ] Graceful degradation tested (Redis failure)
- [ ] Code review requested
- [ ] Merge to main branch
---
## 📚 Referințe
### Documentație Existentă
- **Docker Compose Redis Config:** `docker-compose.yml:147-163`
- **Oracle Pool Pattern:** `shared/database/oracle_pool.py` (reference for singleton pattern)
- **Backend Services:** `backend/app/services/` (where to apply caching)
- **Backend Routers:** `backend/app/routers/` (where to invalidate cache)
### Documentație Externă
- Redis Python Client: https://redis.readthedocs.io/en/stable/
- Redis Commands: https://redis.io/commands/
- FastAPI Async: https://fastapi.tiangolo.com/async/
### Debugging
**Redis CLI Access:**
```bash
docker-compose exec roa-redis redis-cli -a roa2web_redis_password
> KEYS cache:*
> GET cache:default:dashboard_summary:abc123
> DEL cache:default:dashboard_summary:abc123
> FLUSHDB # Delete all keys (WARNING: destructive)
```
**Monitor Redis Operations:**
```bash
docker-compose exec roa-redis redis-cli -a roa2web_redis_password MONITOR
```
**Check Cache Stats:**
```bash
docker-compose exec roa-redis redis-cli -a roa2web_redis_password INFO stats
```
---
## 🎯 Code Templates
### Template: RedisCache Client (`shared/cache/redis_client.py`)
```python
"""
Redis Cache Client - Singleton pattern similar to OraclePool
Provides async Redis operations with graceful degradation
"""
import redis.asyncio as redis
import logging
import json
from typing import Optional, Any
import os
logger = logging.getLogger(__name__)
class RedisCache:
"""Singleton Redis cache client with connection pooling"""
_instance: Optional['RedisCache'] = None
_client: Optional[redis.Redis] = None
_enabled: bool = False
def __new__(cls):
if cls._instance is None:
cls._instance = super(RedisCache, cls).__new__(cls)
return cls._instance
async def initialize(
self,
url: str = None,
password: str = None,
max_connections: int = 50
):
"""Initialize Redis connection pool"""
if self._client is not None:
return
try:
url = url or os.getenv('REDIS_URL', 'redis://localhost:6379/0')
password = password or os.getenv('REDIS_PASSWORD')
self._client = await redis.from_url(
url,
password=password,
encoding="utf-8",
decode_responses=True,
max_connections=max_connections,
socket_connect_timeout=5,
socket_keepalive=True
)
# Test connection
await self._client.ping()
self._enabled = True
logger.info("✅ Redis cache initialized successfully")
except Exception as e:
logger.warning(f"⚠️ Redis initialization failed: {e}. Caching disabled.")
self._enabled = False
self._client = None
def is_enabled(self) -> bool:
"""Check if Redis caching is enabled"""
return self._enabled and self._client is not None
async def get(self, key: str) -> Optional[Any]:
"""Get value from cache"""
if not self.is_enabled():
return None
try:
value = await self._client.get(key)
if value:
return json.loads(value)
return None
except Exception as e:
logger.error(f"Redis GET error for key {key}: {e}")
return None
async def set(self, key: str, value: Any, ttl: int = 300):
"""Set value in cache with TTL"""
if not self.is_enabled():
return
try:
value_json = json.dumps(value, default=str)
await self._client.setex(key, ttl, value_json)
except Exception as e:
logger.error(f"Redis SET error for key {key}: {e}")
async def delete(self, key: str):
"""Delete single key"""
if not self.is_enabled():
return
try:
await self._client.delete(key)
except Exception as e:
logger.error(f"Redis DELETE error for key {key}: {e}")
async def delete_pattern(self, pattern: str):
"""Delete all keys matching pattern (e.g., 'cache:default:dashboard*')"""
if not self.is_enabled():
return
try:
async for key in self._client.scan_iter(match=pattern):
await self._client.delete(key)
logger.debug(f"Deleted keys matching pattern: {pattern}")
except Exception as e:
logger.error(f"Redis DELETE_PATTERN error for {pattern}: {e}")
async def close(self):
"""Close Redis connection"""
if self._client:
await self._client.close()
self._client = None
self._enabled = False
logger.info("✅ Redis connection closed")
# Global singleton instance
redis_cache = RedisCache()
```
---
## ⚠️ Known Limitations & Future Work
### Current Limitations
1. **No Cache Warming:** Cache is cold on startup (first requests slow)
- Future: Implement background task to pre-populate hot keys
2. **Manual Invalidation:** Invalidation must be coded manually in routers
- Future: Auto-invalidation via database triggers or event system
3. **Single Tenant:** All cache keys use `tenant_id="default"`
- Future: Extract tenant_id from JWT token when multi-tenant implemented
4. **No Cache Monitoring:** No dashboard/metrics for cache performance
- Future: Integrate Prometheus metrics (hit/miss rate, latency, memory)
5. **Simple Serialization:** Uses JSON (no support for binary data, datetime needs str conversion)
- Future: Consider msgpack for faster serialization
### Future Enhancements
- [ ] **Cache Warming:** Background task to pre-load hot keys at startup
- [ ] **Smart Invalidation:** Event-driven invalidation based on DB changes
- [ ] **Cache Monitoring Dashboard:** Redis metrics + hit/miss rates
- [ ] **Cache Compression:** Compress large values (>10KB) before storing
- [ ] **Multi-Level Cache:** L1 (in-memory LRU) + L2 (Redis) for ultra-fast access
- [ ] **Cache Tagging:** Tag-based invalidation instead of pattern matching
---
## 📞 Support & Questions
**Dacă întâmpini probleme:**
1. **Redis nu pornește:** Check `docker-compose logs roa-redis`
2. **Connection failed:** Verify REDIS_URL și REDIS_PASSWORD în .env
3. **Cache nu funcționează:** Verify REDIS_ENABLED=true și logs pentru errors
4. **Performance nu se îmbunătățește:** Check cache hit rate în logs
**Contact:** Claude Code Implementation Team
---
**Planul este gata pentru implementare! Începe cu FAZA 1 și urmează pașii exact cum sunt descriși.**