- 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>
911 lines
30 KiB
Markdown
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.**
|