243 lines
7.2 KiB
Go
243 lines
7.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
|
|
internalAPIKey string // X-Internal-API-Key for /internal/jobs/transcode (P1.1.2)
|
|
client *http.Client
|
|
circuitBreaker *CircuitBreakerHTTPClient
|
|
logger *zap.Logger
|
|
}
|
|
|
|
func NewStreamService(baseURL string, logger *zap.Logger) *StreamService {
|
|
return NewStreamServiceWithAPIKey(baseURL, "", logger)
|
|
}
|
|
|
|
// NewStreamServiceWithAPIKey creates a StreamService with optional internal API key for transcode endpoint auth.
|
|
func NewStreamServiceWithAPIKey(baseURL, internalAPIKey string, logger *zap.Logger) *StreamService {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
|
return &StreamService{
|
|
baseURL: baseURL,
|
|
internalAPIKey: internalAPIKey,
|
|
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")
|
|
if s.internalAPIKey != "" {
|
|
req.Header.Set("X-Internal-API-Key", s.internalAPIKey)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
type HLSStatus struct {
|
|
TrackID string `json:"track_id"`
|
|
Status string `json:"status"`
|
|
Progress int `json:"progress"`
|
|
Qualities []string `json:"qualities,omitempty"`
|
|
Duration float64 `json:"duration,omitempty"`
|
|
}
|
|
|
|
// GetHLSStatus queries the stream server for the current HLS transcoding status.
|
|
func (s *StreamService) GetHLSStatus(ctx context.Context, trackID uuid.UUID) (*HLSStatus, error) {
|
|
url := fmt.Sprintf("%s/v1/stream/job/%s", s.baseURL, trackID.String())
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
if s.internalAPIKey != "" {
|
|
req.Header.Set("X-Internal-API-Key", s.internalAPIKey)
|
|
}
|
|
|
|
resp, err := s.circuitBreaker.DoWithContext(ctx, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stream server request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return &HLSStatus{TrackID: trackID.String(), Status: "not_found"}, nil
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("stream server returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var status HLSStatus
|
|
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
return &status, nil
|
|
}
|
|
|
|
type HLSTranscodeRequest struct {
|
|
TrackID string `json:"track_id"`
|
|
FilePath string `json:"file_path"`
|
|
Format string `json:"format"`
|
|
}
|
|
|
|
// TriggerHLSTranscode sends a request to the stream server to start HLS transcoding.
|
|
func (s *StreamService) TriggerHLSTranscode(ctx context.Context, trackID uuid.UUID, filePath string) error {
|
|
url := fmt.Sprintf("%s/internal/jobs/transcode", s.baseURL)
|
|
reqBody := HLSTranscodeRequest{
|
|
TrackID: trackID.String(),
|
|
FilePath: filePath,
|
|
Format: "hls",
|
|
}
|
|
|
|
jsonBody, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if s.internalAPIKey != "" {
|
|
req.Header.Set("X-Internal-API-Key", s.internalAPIKey)
|
|
}
|
|
|
|
resp, err := s.circuitBreaker.DoWithContext(ctx, req)
|
|
if err != nil {
|
|
return fmt.Errorf("stream server request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
|
return fmt.Errorf("stream server returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
s.logger.Info("HLS transcode triggered",
|
|
zap.String("track_id", trackID.String()),
|
|
zap.String("file_path", filePath))
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 ""
|
|
}
|