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 "" }