veza/veza-backend-api/internal/services/stream_service.go

147 lines
4.2 KiB
Go

package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/google/uuid" // Added import for uuid
"go.uber.org/zap"
)
type StreamService struct {
baseURL string
client *http.Client
circuitBreaker *CircuitBreakerHTTPClient
logger *zap.Logger
}
func NewStreamService(baseURL string, logger *zap.Logger) *StreamService {
if logger == nil {
logger = zap.NewNop()
}
httpClient := &http.Client{Timeout: 10 * time.Second}
return &StreamService{
baseURL: baseURL,
client: httpClient,
circuitBreaker: NewCircuitBreakerHTTPClient(httpClient, "stream-service", logger),
logger: logger,
}
}
type TranscodeRequest struct {
TrackID string `json:"track_id"`
FilePath string `json:"file_path"`
}
func (s *StreamService) StartProcessing(ctx context.Context, trackID uuid.UUID, filePath string) error { // Changed trackID to uuid.UUID
url := fmt.Sprintf("%s/internal/jobs/transcode", s.baseURL)
reqBody := TranscodeRequest{
TrackID: trackID.String(), // Converted uuid.UUID to string
FilePath: filePath,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
// MOD-P1-RES-002: Ajouter retry avec backoff exponentiel (pattern similaire à 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 (le body peut être consommé)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
// FIX #11 & #23: Propager le request_id depuis le contexte si disponible
if requestID := extractRequestIDFromContext(ctx); requestID != "" {
req.Header.Set("X-Request-ID", requestID)
s.logger.Debug("Propagating request_id to stream server",
zap.String("request_id", requestID),
zap.String("track_id", trackID.String()),
)
}
// MOD-P2-007: Utiliser circuit breaker pour protéger contre dépendances lentes
resp, err := s.circuitBreaker.DoWithContext(ctx, req)
if err != nil {
s.logger.Warn("Stream server request failed, retrying",
zap.Int("attempt", i+1),
zap.Int("max_retries", maxRetries),
zap.Error(err))
if i < maxRetries-1 {
// Attendre avec backoff exponentiel avant de réessayer
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)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
s.logger.Info("Started processing for track",
zap.Any("track_id", trackID),
zap.Int("attempt", i+1))
return nil
}
// Status code non-OK : retry si possible
s.logger.Warn("Stream server returned non-200 status",
zap.Int("status", resp.StatusCode),
zap.Int("attempt", i+1),
zap.Int("max_retries", maxRetries))
if i < maxRetries-1 {
// Attendre avec backoff exponentiel avant de réessayer
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
}
}
}
return fmt.Errorf("stream server returned non-200 status after %d attempts", maxRetries)
}
// extractRequestIDFromContext extrait le request_id depuis un contexte Go
// FIX #11: Utilise la même logique que middleware.GetRequestIDFromGoContext mais sans cycle d'import
func extractRequestIDFromContext(ctx context.Context) string {
// Essayer différentes clés possibles
keys := []interface{}{"request_id", "X-Request-ID", "requestId"}
for _, key := range keys {
if value := ctx.Value(key); value != nil {
if id, ok := value.(string); ok && id != "" {
return id
}
}
}
return ""
}