package service import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/repository" "context" "log/slog" "time" ) type RPUsageProjectorWorker struct { outbox repository.RPUsageOutboxRepository projection domain.RPUsageProjectionRepository interval time.Duration batchSize int } func NewRPUsageProjectorWorker(outbox repository.RPUsageOutboxRepository, projection domain.RPUsageProjectionRepository) *RPUsageProjectorWorker { return &RPUsageProjectorWorker{ outbox: outbox, projection: projection, interval: 5 * time.Second, batchSize: 50, } } func (w *RPUsageProjectorWorker) Start(ctx context.Context) { if w == nil || w.outbox == nil || w.projection == nil { return } ticker := time.NewTicker(w.interval) defer ticker.Stop() for { select { case <-ctx.Done(): return default: w.processOnce(ctx) } select { case <-ctx.Done(): return case <-ticker.C: } } } func (w *RPUsageProjectorWorker) processOnce(ctx context.Context) { events, err := w.outbox.ListReady(ctx, w.batchSize) if err != nil { slog.Warn("failed to list rp usage outbox", "error", err) return } for _, event := range events { if err := w.outbox.MarkProcessing(ctx, event.ID); err != nil { slog.Warn("failed to mark rp usage event processing", "event_id", event.ID, "error", err) continue } if err := w.projection.CreateRPUsageEvent(ctx, event); err != nil { nextAttempt := time.Now().Add(backoffDuration(event.RetryCount)) _ = w.outbox.MarkFailed(ctx, event.ID, err.Error(), nextAttempt) slog.Warn("failed to project rp usage event", "event_id", event.ID, "error", err) continue } if err := w.outbox.MarkProcessed(ctx, event.ID); err != nil { slog.Warn("failed to mark rp usage event processed", "event_id", event.ID, "error", err) } } } func backoffDuration(retryCount int) time.Duration { if retryCount < 0 { retryCount = 0 } delay := time.Duration(retryCount+1) * time.Minute if delay > 30*time.Minute { return 30 * time.Minute } return delay }