#26 — dba-daemon
PLANIFIÉ
Priorité: 🔴 CRITIQUE · Type: TYPE B+C · Conteneurs: rgz-api + rgz-beat · Code: app/services/dba.py + app/tasks/qos.py
Dépendances: #6 rgz-radius · #4 rgz-db · #10 rgz-beat
Description
Le DBA Daemon (Dynamic Bandwidth Allocation) est le moteur d'allocation dynamique de bande passante du réseau ACCESS. Il recalcule toutes les 5 minutes le MIR (Maximum Information Rate) de chaque revendeur actif en fonction du trafic réseau réel mesuré, puis applique les nouveaux paramètres de débit sur les CPE via le protocole CoA (Change of Authorization) RFC 5176.
L'objectif du DBA est double : garantir une expérience équitable entre revendeurs en période de congestion, et permettre le burst (consommation au-delà du CIR contractuel) lorsque la capacité réseau est disponible. Ce mécanisme est transparent pour l'abonné final — il bénéficie automatiquement de la bande passante disponible sans intervention manuelle.
Le DBA opère en deux couches complémentaires. La couche service (app/services/dba.py) contient la logique de calcul MIR, l'envoi CoA via FreeRADIUS, et la mise en cache Redis. La couche tâche Celery (app/tasks/qos.py) orchestre le déclenchement périodique via rgz-beat toutes les 5 minutes, avec gestion des erreurs et retry exponentiel.
L'ordre strict DB → Redis → CoA (LL#26) est imposé : les nouvelles valeurs MIR sont d'abord persistées en base, puis mise en cache Redis, et enfin envoyées au CPE. En cas d'échec CoA, le CPE conserve sa dernière valeur MIR valide — l'état DB reste la source de vérité.
Architecture Interne
rgz-beat (every 5min)
│
▼
app/tasks/qos.py :: recalculate_dba()
│
├──► Lecture sessions actives : radius_sessions WHERE is_active=True
│ GROUP BY reseller_id → {session_count, bytes_in_rate, bytes_out_rate}
│
├──► Calcul charge réseau globale (sum bytes / QOS_TOTAL_MBPS)
│ → load_percent : 0-100%
│
├──► Pour chaque revendeur :
│ app/services/dba.py :: compute_mir(reseller_id, load_percent)
│ ┌─────────────────────────────────────────────────────────┐
│ │ CIR = forfait.cir_mbps (ex: 2 Mbps contractuel) │
│ │ Si load < 50% → MIR = CIR × 3 (burst max) │
│ │ Si load 50-80% → MIR = CIR × 1.5 (burst modéré) │
│ │ Si load > 80% → MIR = CIR × 1 (congestion mode) │
│ └─────────────────────────────────────────────────────────┘
│
├──► LL#26 : WRITE DB FIRST
│ UPDATE resellers SET current_mir_mbps=X, mir_updated_at=NOW()
│
├──► THEN Redis cache
│ HSET rgz:dba:mir:{reseller_id} mir_mbps X updated_at T session_count N
│ EXPIRE rgz:dba:mir:{reseller_id} 600
│
└──► THEN CoA RFC 5176 → FreeRADIUS → CPE
radclient -x {radius_host}:3799 coa {radius_secret}
Bandwidth-Max-Up = MIR × 1000000 (bits/s)
Bandwidth-Max-Down = MIR × 1000000 (bits/s)
NAS-Identifier = nas_id du revendeurConfiguration
Variables d'environnement
| Variable | Valeur exemple | Description |
|---|---|---|
RADIUS_SECRET | secret_radius_rgz | Secret CoA (SEC-05 : env var uniquement) |
RADIUS_HOST | rgz-radius | Hostname FreeRADIUS |
RADIUS_COA_PORT | 3799 | Port CoA RFC 5176 |
DBA_INTERVAL_SEC | 300 | Intervalle recalcul (5min) |
DBA_BURST_LOW | 3.0 | Multiplicateur MIR si charge <50% |
DBA_BURST_MED | 1.5 | Multiplicateur MIR si charge 50-80% |
DBA_CONGESTION_THRESHOLD | 80 | Seuil % charge → congestion mode |
QOS_TOTAL_MBPS | 100 | Capacité WAN totale pour calcul load% |
Celery Task
# app/tasks/qos.py
from app.celery_app import celery_app
@celery_app.task(
name="rgz.dba.recalculate",
queue="rgz.qos",
autoretry_for=(Exception,),
retry_backoff=True,
max_retries=3
)
def recalculate_dba():
"""
Tâche Celery Beat — every 5min — queue rgz.qos
Recalcule le MIR de chaque revendeur actif et envoie CoA.
"""
from app.services.dba import DBAService
service = DBAService()
return service.run_full_cycle()# app/celery_app.py — beat schedule entry
"rgz.dba.recalculate": {
"task": "rgz.dba.recalculate",
"schedule": 300.0, # 5 minutes
"options": {"queue": "rgz.qos"},
},Endpoints API
| Méthode | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/v1/qos/dba/{reseller_id} | Bearer JWT | Lire MIR actuel du revendeur |
| POST | /api/v1/qos/dba/{reseller_id}/force | Bearer JWT (admin) | Forcer recalcul immédiat |
| GET | /api/v1/qos/dba | Bearer JWT (admin) | Liste MIR tous revendeurs |
Exemple — GET /api/v1/qos/dba/
// Requête
GET /api/v1/qos/dba/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer eyJ...
// Réponse 200
{
"reseller_id": "550e8400-e29b-41d4-a716-446655440000",
"cir_mbps": 2,
"mir_mbps": 6,
"load_percent": 35.2,
"multiplier": 3.0,
"session_count": 4,
"updated_at": "2026-02-21T14:30:00Z",
"coa_sent": true,
"coa_sent_at": "2026-02-21T14:30:01Z"
}Exemple — POST /api/v1/qos/dba/{reseller_id}/force
// Requête (admin uniquement)
POST /api/v1/qos/dba/550e8400-e29b-41d4-a716-446655440000/force
Authorization: Bearer eyJ... (role=admin)
// Réponse 202
{
"message": "DBA recalculation triggered",
"task_id": "abc123-celery-task-id",
"reseller_id": "550e8400-e29b-41d4-a716-446655440000"
}Redis Keys
| Clé Redis | Type | TTL | Contenu |
|---|---|---|---|
rgz:dba:mir:{reseller_id} | Hash | 600s | mir_mbps, cir_mbps, load_percent, updated_at, session_count |
rgz:dba:lock:cycle | String | 60s | Mutex anti-doublon run Celery |
# Structure Hash Redis
{
"mir_mbps": "6",
"cir_mbps": "2",
"load_percent": "35.2",
"updated_at": "2026-02-21T14:30:00Z",
"session_count": "4",
"coa_sent": "true"
}Celery Tasks
| Tâche | Queue | Intervalle | Description |
|---|---|---|---|
rgz.dba.recalculate | rgz.qos | 5 min | Recalcul MIR + envoi CoA tous revendeurs |
Commandes Utiles
# Vérifier le MIR actuel d'un revendeur via Redis
docker exec rgz-redis redis-cli HGETALL "rgz:dba:mir:550e8400-e29b-41d4-a716-446655440000"
# Lancer manuellement la tâche DBA (debug)
docker exec rgz-beat celery -A app.celery_app call rgz.dba.recalculate
# Voir les logs de la tâche DBA
docker logs rgz-beat --tail=50 | grep "dba"
# Vérifier qu'un CoA est bien envoyé
docker exec rgz-radius radclient -x rgz-radius:3799 coa secret \
"NAS-Identifier=access_kossou,Bandwidth-Max-Down=6000000,Bandwidth-Max-Up=6000000"
# Inspecter la queue Celery qos
docker exec rgz-redis redis-cli LLEN "rgz.qos"
# Forcer recalcul via API (admin)
curl -X POST https://api-rgz.duckdns.org/api/v1/qos/dba/{reseller_id}/force \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Vérifier état DB MIR
docker exec rgz-db psql -U rgz -d rgzdb -c \
"SELECT id, slug, current_mir_mbps, mir_updated_at FROM resellers WHERE status='active';"Sécurité
| Règle | Application |
|---|---|
| SEC-05 | RADIUS_SECRET uniquement via variable d'environnement, jamais codé en dur dans dba.py |
| LL#26 | Ordre strict : écriture DB → mise à jour Redis → envoi CoA. Si CoA échoue : log + retry, pas de rollback DB |
| SEC-01 | Endpoint GET /qos/dba/{reseller_id} vérifie que current_user.reseller_id == reseller_id sauf si role=admin |
| SEC-07 | Redis protégé par requirepass (variable REDIS_PASSWORD), ACL limitée au service api |
Gestion des Erreurs CoA
# app/services/dba.py — gestion échec CoA
try:
coa_result = send_coa(nas_id, mir_mbps)
logger.info(f"CoA sent to {nas_id}: MIR={mir_mbps}Mbps")
except CoAException as e:
# CPE garde son ancienne valeur MIR — pas critique
logger.warning(f"CoA failed for {nas_id}: {e}. DB value preserved.")
# NE PAS lever d'exception — la tâche Celery ne doit pas échouer pour un CoA ratéImplémentation TODO
- [ ] Créer
app/services/dba.pyavec classeDBAService:compute_mir(),send_coa(),run_full_cycle() - [ ] Créer
app/tasks/qos.pyavec tâchergz.dba.recalculate - [ ] Ajouter colonnes
current_mir_mbps INTEGER,mir_updated_at TIMESTAMPTZà tableresellers - [ ] Ajouter colonne
cir_mbps INTEGER NOT NULL DEFAULT 2à tableresellers - [ ] Implémenter endpoint
GET /api/v1/qos/dba/{reseller_id}(lecture Redis → fallback DB) - [ ] Implémenter endpoint
POST /api/v1/qos/dba/{reseller_id}/force(admin only) - [ ] Configurer beat schedule dans
app/celery_app.py(rgz.dba.recalculate every 300s) - [ ] Tests unitaires
compute_mir()pour les 3 seuils de charge - [ ] Tests intégration CoA avec FreeRADIUS mock
- [ ] Vérifier SEC-05 : aucun hardcode
RADIUS_SECRETdans le code
Dernière mise à jour: 2026-02-21