9.6 KiB
✅ P1-RES-002 — RETRY MÉCANISME POUR STREAMSERVICE
Date: 2025-12-13
Objectif: Ajouter un mécanisme de retry avec backoff exponentiel pour StreamService afin d'éviter la perte de jobs si le stream-server est temporairement indisponible
📋 RÉSUMÉ
✅ Retry implémenté : Pattern similaire à WebhookService (3 tentatives, backoff exponentiel)
✅ Respect du contexte : Vérification du contexte avant chaque tentative et pendant le backoff
✅ Tests complets : 6 tests couvrent tous les scénarios (retry, max retries, erreurs réseau, timeout, cancellation, backoff timing)
✅ Compilation réussie : Aucune erreur de compilation
📁 FICHIERS MODIFIÉS
1. internal/services/stream_service.go
- ✅ Méthode
StartProcessingmodifiée : Ajout du mécanisme de retry avec backoff exponentiel - ✅ Pattern identique à
WebhookService: Réutilisation du pattern existant (maxRetries=3, backoff exponentiel) - ✅ Respect du contexte : Vérification du contexte avant chaque tentative et pendant le backoff
2. internal/services/stream_service_retry_test.go (nouveau)
- ✅ 6 tests complets :
TestStreamService_StartProcessing_RetryOnFailure: Retry fonctionne quand le serveur échoue temporairementTestStreamService_StartProcessing_MaxRetriesExceeded: Échec après max retriesTestStreamService_StartProcessing_RetryOnNetworkError: Retry sur erreurs réseauTestStreamService_StartProcessing_ContextTimeout: Respect du timeout du contexteTestStreamService_StartProcessing_ContextCancelledDuringBackoff: Annulation pendant le backoffTestStreamService_StartProcessing_BackoffTiming: Vérification du timing du backoff exponentiel
🔍 IMPLÉMENTATION
Pattern de retry (identique à WebhookService)
maxRetries := 3
backoff := time.Second
for i := 0; i < maxRetries; i++ {
// Vérifier si le contexte est annulé avant chaque tentative
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled before attempt %d: %w", i+1, ctx.Err())
default:
}
// Créer une nouvelle requête pour chaque tentative
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
// ...
resp, err := s.client.Do(req)
if err != nil {
// Log et retry si possible
if i < maxRetries-1 {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled during backoff: %w", ctx.Err())
case <-time.After(backoff):
backoff *= 2 // Exponential backoff: 1s, 2s, 4s
}
continue
}
return fmt.Errorf("stream server request failed after %d attempts: %w", maxRetries, err)
}
if resp.StatusCode == http.StatusOK {
return nil // Succès
}
// Status code non-OK : retry si possible
if i < maxRetries-1 {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled during backoff: %w", ctx.Err())
case <-time.After(backoff):
backoff *= 2 // Exponential backoff: 1s, 2s, 4s
}
}
}
Comportement exact
N tentatives : 3 maximum (configurable via maxRetries)
Backoff exponentiel :
- Tentative 1 : Immédiate
- Tentative 2 : Après 1 seconde (backoff initial)
- Tentative 3 : Après 2 secondes (backoff * 2)
- Total minimum : ~3 secondes (1s + 2s)
Erreurs finales :
- Si toutes les tentatives échouent :
"stream server request failed after 3 attempts: <error>" - Si le contexte est annulé :
"context cancelled before attempt N: <error>"ou"context cancelled during backoff: <error>" - Si le status code est non-OK après toutes les tentatives :
"stream server returned non-200 status after 3 attempts"
Respect du contexte :
- Vérification avant chaque tentative
- Vérification pendant le backoff (avec
selectsurctx.Done()) - Annulation immédiate si le contexte est annulé
🧪 PREUVES (TESTS)
Tests unitaires
go test ./internal/services -run TestStreamService_StartProcessing_Retry -v -count=1
Résultat : ✅ Tous les tests passent (6/6)
Test 1 : Retry sur échec temporaire
TestStreamService_StartProcessing_RetryOnFailure
- ✅ Mock serveur échoue 2 fois puis réussit
- ✅ Vérifie que 3 tentatives sont faites
- ✅ Vérifie que la requête réussit finalement
Test 2 : Max retries atteint
TestStreamService_StartProcessing_MaxRetriesExceeded
- ✅ Mock serveur échoue toujours
- ✅ Vérifie que exactement 3 tentatives sont faites
- ✅ Vérifie que l'erreur mentionne "after 3 attempts"
Test 3 : Retry sur erreur réseau
TestStreamService_StartProcessing_RetryOnNetworkError
- ✅ Mock serveur ferme la connexion (simule erreur réseau)
- ✅ Vérifie que le retry fonctionne pour les erreurs réseau
- ✅ Vérifie que 3 tentatives sont faites
Test 4 : Timeout du contexte
TestStreamService_StartProcessing_ContextTimeout
- ✅ Mock serveur prend trop de temps
- ✅ Contexte avec timeout de 500ms
- ✅ Vérifie que l'erreur mentionne "context"
Test 5 : Annulation pendant backoff
TestStreamService_StartProcessing_ContextCancelledDuringBackoff
- ✅ Mock serveur échoue toujours
- ✅ Contexte annulé après le premier échec (pendant le backoff)
- ✅ Vérifie que l'erreur mentionne "context cancelled"
- ✅ Vérifie que seulement 1 tentative est faite
Test 6 : Timing du backoff
TestStreamService_StartProcessing_BackoffTiming
- ✅ Mock serveur échoue 2 fois puis réussit
- ✅ Vérifie que le timing total est ~3s minimum
- ✅ Vérifie que les intervalles entre tentatives sont corrects (1s, 2s)
Tests complets
go test ./internal/services -v -count=1
Résultat : ✅ Tous les tests passent
📊 COMPARAISON AVANT/APRÈS
Avant (Sans retry)
resp, err := s.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err) // ❌ Pas de retry
}
Problèmes :
- ❌ Une panne temporaire du stream-server cause la perte du job
- ❌ Pas de résilience face aux erreurs réseau temporaires
- ❌ État incohérent si le job est créé mais le stream-server ne le reçoit pas
Après (Avec retry)
maxRetries := 3
backoff := time.Second
for i := 0; i < maxRetries; i++ {
// Vérification contexte + retry avec backoff
// ...
}
Avantages :
- ✅ Résilience face aux pannes temporaires (3 tentatives)
- ✅ Backoff exponentiel pour éviter la surcharge
- ✅ Respect du contexte (annulation possible)
- ✅ Logs détaillés pour debugging
- ✅ Pattern cohérent avec
WebhookService
✅ VALIDATION
Compilation
go build ./internal/services/...
Résultat : ✅ Compilation réussie
Tests unitaires
go test ./internal/services -run TestStreamService_StartProcessing_Retry -v
Résultat : ✅ Tous les tests passent (6/6)
Tests complets
go test ./internal/services -v -count=1
Résultat : ✅ Tous les tests passent
Tests existants (non-régression)
go test ./internal/services -run TestStreamService_StartProcessing -v
Résultat : ✅ Tous les tests existants passent
🎯 OBJECTIFS ATTEINTS
- ✅ Retry implémenté : 3 tentatives avec backoff exponentiel (pattern identique à
WebhookService) - ✅ Respect du contexte : Vérification avant chaque tentative et pendant le backoff
- ✅ Tests complets : 6 tests couvrent tous les scénarios (retry, max retries, erreurs réseau, timeout, cancellation, backoff timing)
- ✅ Non-régression : Tous les tests existants passent toujours
- ✅ Pattern réutilisé : Aucune nouvelle dépendance, réutilisation du pattern existant
📋 COMMANDES DE VALIDATION
Compilation
go build ./internal/services/...
Tests spécifiques
go test ./internal/services -run TestStreamService_StartProcessing_Retry -v -count=1
go test ./internal/services -run TestStreamService_StartProcessing -v -count=1
Tests complets
go test ./internal/services -v -count=1
📝 COMPORTEMENT EXACT
N tentatives
Maximum : 3 tentatives (configurable via maxRetries)
Scénarios :
- Succès à la tentative N : Retourne immédiatement avec succès
- Échec après 3 tentatives : Retourne une erreur avec message "after 3 attempts"
- Annulation du contexte : Retourne immédiatement avec erreur "context cancelled"
Backoff exponentiel
Séquence :
- Tentative 1 : Immédiate (pas de backoff)
- Tentative 2 : Après 1 seconde (backoff initial = 1s)
- Tentative 3 : Après 2 secondes (backoff * 2 = 2s)
Total minimum : ~3 secondes (1s + 2s) si toutes les tentatives échouent
Respect du contexte : Le backoff peut être interrompu si le contexte est annulé
Erreurs finales
Format :
- Erreur réseau après max retries :
"stream server request failed after 3 attempts: <error>" - Status code non-OK après max retries :
"stream server returned non-200 status after 3 attempts" - Contexte annulé avant tentative :
"context cancelled before attempt N: <error>" - Contexte annulé pendant backoff :
"context cancelled during backoff: <error>"
Logs :
- Tentative échouée :
Warnavecattempt,max_retries,error - Succès :
Infoavectrack_id,attempt
Statut final : ✅ P1-RES-002 IMPLÉMENTÉ ET VALIDÉ
Note : Le mécanisme de retry est maintenant identique à celui de WebhookService, assurant une cohérence dans la gestion des appels HTTP externes.