package workers import ( "context" "fmt" "time" "veza-backend-api/internal/models" "veza-backend-api/internal/services" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" ) // WebhookWorker gère les webhooks en arrière-plan type WebhookWorker struct { db *gorm.DB webhookService *services.WebhookService logger *zap.Logger queue chan WebhookJob maxRetries int processingWorkers int } // WebhookJob représente une tâche de webhook à traiter type WebhookJob struct { ID uuid.UUID Webhook *models.Webhook Event string Data map[string]interface{} Retries int CreatedAt time.Time } // NewWebhookWorker crée un nouveau worker de webhooks func NewWebhookWorker( db *gorm.DB, webhookService *services.WebhookService, logger *zap.Logger, queueSize int, workers int, maxRetries int, ) *WebhookWorker { return &WebhookWorker{ db: db, webhookService: webhookService, logger: logger, queue: make(chan WebhookJob, queueSize), maxRetries: maxRetries, processingWorkers: workers, } } // Enqueue ajoute un job au queue func (w *WebhookWorker) Enqueue(job WebhookJob) { job.CreatedAt = time.Now() job.ID = uuid.New() select { case w.queue <- job: w.logger.Debug("Webhook job enqueued", zap.String("job_id", job.ID.String()), zap.String("event", job.Event), zap.String("webhook_url", job.Webhook.URL)) default: w.logger.Warn("Webhook queue full, dropping job", zap.String("event", job.Event)) } } // Start démarre le worker func (w *WebhookWorker) Start(ctx context.Context) { w.logger.Info("Starting webhook worker", zap.Int("workers", w.processingWorkers)) for i := 0; i < w.processingWorkers; i++ { go w.processWorker(ctx, i) } } // processWorker traite les jobs du queue func (w *WebhookWorker) processWorker(ctx context.Context, workerID int) { w.logger.Info("Webhook worker started", zap.Int("worker_id", workerID)) for { select { case <-ctx.Done(): w.logger.Info("Webhook worker stopping", zap.Int("worker_id", workerID)) return case job := <-w.queue: w.processJob(ctx, job, workerID) } } } // processJob traite un job individuel func (w *WebhookWorker) processJob(ctx context.Context, job WebhookJob, workerID int) { logger := w.logger.With( zap.String("job_id", job.ID.String()), zap.String("event", job.Event), zap.Int("worker_id", workerID)) logger.Info("Processing webhook job", zap.Int("retries", job.Retries)) // Créer un contexte avec timeout pour la livraison deliveryCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() // Tenter de livrer le webhook err := w.webhookService.DeliverWebhook( deliveryCtx, job.Webhook, job.Event, job.Data, ) if err != nil { logger.Error("Webhook delivery failed", zap.Error(err)) // Enregistrer l'échec dans la table de logs w.logFailedDelivery(ctx, job, err) // Retry si pas atteint max retries if job.Retries < w.maxRetries { job.Retries++ // Exponential backoff via time.AfterFunc (non-blocking) delay := time.Duration(job.Retries) * time.Second time.AfterFunc(delay, func() { // Ré-enqueue le job w.Enqueue(job) logger.Info("Retrying webhook delivery (enqueued)", zap.Int("new_retries", job.Retries)) }) } else { logger.Error("Webhook delivery failed after max retries", zap.Int("max_retries", w.maxRetries)) } } else { logger.Info("Webhook delivered successfully") } } // logFailedDelivery enregistre un échec de livraison func (w *WebhookWorker) logFailedDelivery(ctx context.Context, job WebhookJob, err error) { failure := &models.WebhookFailure{ WebhookID: job.Webhook.ID, Event: job.Event, Error: err.Error(), Retries: job.Retries, CreatedAt: time.Now(), } if err := w.db.WithContext(ctx).Create(failure).Error; err != nil { w.logger.Error("Failed to log webhook failure", zap.Error(err), zap.String("job_id", job.ID.String())) } } // GetStats retourne les statistiques du worker func (w *WebhookWorker) GetStats() map[string]interface{} { return map[string]interface{}{ "queue_size": len(w.queue), "workers": w.processingWorkers, "max_retries": w.maxRetries, } } // CleanupOldFailures supprime les anciennes pannes de livraison func (w *WebhookWorker) CleanupOldFailures(ctx context.Context, daysOld int) error { cutoffDate := time.Now().AddDate(0, 0, -daysOld) result := w.db.WithContext(ctx). Where("created_at < ?", cutoffDate). Delete(&models.WebhookFailure{}) if result.Error != nil { return fmt.Errorf("failed to cleanup old failures: %w", result.Error) } w.logger.Info("Cleaned up old webhook failures", zap.Int("rows_deleted", int(result.RowsAffected)), zap.Int("days_old", daysOld)) return nil } // RequeueFailed retente les webhooks en échec func (w *WebhookWorker) RequeueFailed(ctx context.Context, failure *models.WebhookFailure) error { // Récupérer le webhook var webhook models.Webhook if err := w.db.WithContext(ctx). First(&webhook, failure.WebhookID).Error; err != nil { return fmt.Errorf("failed to fetch webhook: %w", err) } // Enqueue à nouveau job := WebhookJob{ Webhook: &webhook, Event: failure.Event, Data: map[string]interface{}{}, // On perd les données originales Retries: failure.Retries, } w.Enqueue(job) w.logger.Info("Requeued failed webhook", zap.String("webhook_id", failure.WebhookID.String()), zap.Int("retries", failure.Retries)) return nil }