# ✅ 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 `StartProcessing` modifié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** : 1. `TestStreamService_StartProcessing_RetryOnFailure` : Retry fonctionne quand le serveur échoue temporairement 2. `TestStreamService_StartProcessing_MaxRetriesExceeded` : Échec après max retries 3. `TestStreamService_StartProcessing_RetryOnNetworkError` : Retry sur erreurs réseau 4. `TestStreamService_StartProcessing_ContextTimeout` : Respect du timeout du contexte 5. `TestStreamService_StartProcessing_ContextCancelledDuringBackoff` : Annulation pendant le backoff 6. `TestStreamService_StartProcessing_BackoffTiming` : Vérification du timing du backoff exponentiel --- ## 🔍 IMPLÉMENTATION ### Pattern de retry (identique à WebhookService) ```go 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: "` - Si le contexte est annulé : `"context cancelled before attempt N: "` ou `"context cancelled during backoff: "` - 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 `select` sur `ctx.Done()`) - Annulation immédiate si le contexte est annulé --- ## 🧪 PREUVES (TESTS) ### Tests unitaires ```bash 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 ```go 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 ```go 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 ```go 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 ```go 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 ```go 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 ```go 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 ```bash go test ./internal/services -v -count=1 ``` **Résultat** : ✅ **Tous les tests passent** --- ## 📊 COMPARAISON AVANT/APRÈS ### Avant (Sans retry) ```go 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) ```go 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 ```bash go build ./internal/services/... ``` **Résultat** : ✅ **Compilation réussie** ### Tests unitaires ```bash go test ./internal/services -run TestStreamService_StartProcessing_Retry -v ``` **Résultat** : ✅ **Tous les tests passent (6/6)** ### Tests complets ```bash go test ./internal/services -v -count=1 ``` **Résultat** : ✅ **Tous les tests passent** ### Tests existants (non-régression) ```bash 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 ```bash go build ./internal/services/... ``` ### Tests spécifiques ```bash go test ./internal/services -run TestStreamService_StartProcessing_Retry -v -count=1 go test ./internal/services -run TestStreamService_StartProcessing -v -count=1 ``` ### Tests complets ```bash 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: "` - 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: "` - Contexte annulé pendant backoff : `"context cancelled during backoff: "` **Logs** : - Tentative échouée : `Warn` avec `attempt`, `max_retries`, `error` - Succès : `Info` avec `track_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.