package worker import ( "context" "sync" "time" "github.com/orchard9/rdev/internal/logging" ) // CheckoutCleanupService defines the interface for checkout cleanup operations. // This allows the worker to depend on the service interface rather than the concrete type. type CheckoutCleanupService interface { CleanupExpired(ctx context.Context) (int, error) } // CheckoutCleanup runs periodic cleanup of expired checkouts. // Expired checkouts have their Gitea tokens revoked and status updated. type CheckoutCleanup struct { service CheckoutCleanupService cleanupInterval time.Duration ctx context.Context cancel context.CancelFunc wg sync.WaitGroup } // CheckoutCleanupConfig holds configuration for checkout cleanup. type CheckoutCleanupConfig struct { // CleanupInterval is how often to run cleanup. // Default: 5 minutes. CleanupInterval time.Duration } // DefaultCheckoutCleanupConfig returns sensible defaults. func DefaultCheckoutCleanupConfig() *CheckoutCleanupConfig { return &CheckoutCleanupConfig{ CleanupInterval: 5 * time.Minute, } } // NewCheckoutCleanup creates a new checkout cleanup worker. func NewCheckoutCleanup(service CheckoutCleanupService, cfg *CheckoutCleanupConfig) *CheckoutCleanup { if cfg == nil { cfg = DefaultCheckoutCleanupConfig() } ctx, cancel := context.WithCancel(context.Background()) return &CheckoutCleanup{ service: service, cleanupInterval: cfg.CleanupInterval, ctx: ctx, cancel: cancel, } } // Start begins the cleanup loop. func (c *CheckoutCleanup) Start() { log := logging.FromContext(c.ctx).WithWorker("checkout-cleanup") log.Info("checkout cleanup started", "cleanup_interval", c.cleanupInterval, ) c.wg.Add(1) go c.cleanupLoop() } // Stop gracefully shuts down the cleanup worker. func (c *CheckoutCleanup) Stop() { log := logging.FromContext(c.ctx).WithWorker("checkout-cleanup") log.Info("checkout cleanup stopping") c.cancel() c.wg.Wait() log.Info("checkout cleanup stopped") } // cleanupLoop runs periodic cleanup. func (c *CheckoutCleanup) cleanupLoop() { defer c.wg.Done() // Run immediately on start c.runCleanup() ticker := time.NewTicker(c.cleanupInterval) defer ticker.Stop() for { select { case <-c.ctx.Done(): return case <-ticker.C: c.runCleanup() } } } // runCleanup marks expired checkouts and revokes their tokens. func (c *CheckoutCleanup) runCleanup() { ctx, cancel := context.WithTimeout(c.ctx, TimeoutMaintenance) defer cancel() log := logging.FromContext(ctx).WithWorker("checkout-cleanup") cleaned, err := c.service.CleanupExpired(ctx) if err != nil { log.Error("failed to cleanup expired checkouts", logging.FieldError, err, ) return } if cleaned > 0 { log.Info("cleaned up expired checkouts", "count", cleaned, ) } }