package service import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/repository" "context" "log/slog" "time" ) type KetoRelayWorker interface { Start(ctx context.Context) } type ketoRelayWorker struct { outboxRepo repository.KetoOutboxRepository ketoService KetoService interval time.Duration maxRetries int } func NewKetoRelayWorker(outboxRepo repository.KetoOutboxRepository, ketoService KetoService) KetoRelayWorker { return &ketoRelayWorker{ outboxRepo: outboxRepo, ketoService: ketoService, interval: 5 * time.Second, // Poll every 5 seconds maxRetries: 5, } } func (w *ketoRelayWorker) Start(ctx context.Context) { slog.Info("[KetoRelayWorker] Starting worker...") ticker := time.NewTicker(w.interval) defer ticker.Stop() for { select { case <-ctx.Done(): slog.Info("[KetoRelayWorker] Stopping worker...") return case <-ticker.C: w.processEntries(ctx) } } } func (w *ketoRelayWorker) processEntries(ctx context.Context) { entries, err := w.outboxRepo.FindPending(ctx, 50) // Process up to 50 at once if err != nil { slog.Error("[KetoRelayWorker] Failed to fetch pending entries", "error", err) return } for _, entry := range entries { w.processEntry(ctx, entry) } } func (w *ketoRelayWorker) processEntry(ctx context.Context, entry domain.KetoOutbox) { var err error if entry.Action == domain.KetoOutboxActionCreate { err = w.ketoService.CreateRelation(ctx, entry.Namespace, entry.Object, entry.Relation, entry.Subject) } else if entry.Action == domain.KetoOutboxActionDelete { err = w.ketoService.DeleteRelation(ctx, entry.Namespace, entry.Object, entry.Relation, entry.Subject) } if err != nil { slog.Error("[KetoRelayWorker] Failed to process entry", "id", entry.ID, "error", err) newRetryCount := entry.RetryCount + 1 status := domain.KetoOutboxStatusPending if newRetryCount >= w.maxRetries { status = domain.KetoOutboxStatusFailed } _ = w.outboxRepo.UpdateStatus(ctx, entry.ID, status, newRetryCount, err.Error()) } else { _ = w.outboxRepo.MarkProcessed(ctx, entry.ID) } }