319 lines
9.6 KiB
Markdown
319 lines
9.6 KiB
Markdown
|
|
# ✅ 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: <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
|
||
|
|
|
||
|
|
```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: <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.
|