veza/veza-backend-api/P1_RES_002_STREAM_SERVICE_RETRY_REPORT.md
2025-12-12 21:34:34 -05:00

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 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)

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 select sur ctx.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 : 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.