Add comprehensive multi-tenant architecture upgrade plan
Creates detailed 60-page implementation roadmap for transforming ROA2WEB from single-tenant to multi-tenant SaaS architecture. Plan includes 6 phases with backward compatibility, hybrid connection support (SSH tunnel + direct), and complete deployment strategies for dev/Docker/Windows environments. Key features: - Tenant isolation with separate Oracle connection pools per tenant - Dynamic SSH tunnel management with auto-restart - Encrypted credentials in PostgreSQL/SQLite tenant config DB - JWT-based tenant identification and access validation - Redis cache namespacing per tenant - Comprehensive testing and migration strategies Timeline: 14-20 days implementation Target: <10% performance overhead, zero downtime migration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2501
shared/docs/MULTI_TENANT_UPGRADE_PLAN.md
Normal file
2501
shared/docs/MULTI_TENANT_UPGRADE_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
910
shared/docs/REDIS_IMPLEMENTATION_PLAN.md
Normal file
910
shared/docs/REDIS_IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,910 @@
|
||||
# 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)
|
||||
|
||||
- [ ] `reports-app/backend/requirements.txt` - Adaugă redis>=5.0.0
|
||||
- [ ] `reports-app/backend/.env.example` - Adaugă REDIS_* env vars
|
||||
- [ ] `reports-app/backend/app/main.py` - Initialize Redis at startup
|
||||
- [ ] `reports-app/backend/app/services/dashboard_service.py` - Apply caching
|
||||
- [ ] `reports-app/backend/app/services/invoice_service.py` - Apply caching
|
||||
- [ ] `reports-app/backend/app/routers/dashboard.py` - Cache invalidation on mutations
|
||||
- [ ] `reports-app/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: `reports-app/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: `reports-app/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: `reports-app/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: `reports-app/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: `reports-app/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: `reports-app/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: `reports-app/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: `reports-app/backend/app/services/invoice_service.py`
|
||||
- Acțiune: TTL=180 (3 min pentru summary)
|
||||
|
||||
6. [ ] **Cache invalidation în dashboard mutations (viitor)**
|
||||
- Fișier: `reports-app/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: `reports-app/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:** `reports-app/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: `reports-app/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:** `reports-app/backend/app/services/` (where to apply caching)
|
||||
- **Backend Routers:** `reports-app/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.**
|
||||
Reference in New Issue
Block a user