package worker import ( "context" "sync" "time" "github.com/orchard9/rdev/internal/logging" ) // SessionCleanupService defines the interface for session cleanup operations. type SessionCleanupService interface { CleanupExpired(ctx context.Context) (int, error) } // SessionCleanup runs periodic cleanup of expired sessions. // Expired sessions have their preview resources torn down and checkout tokens revoked. type SessionCleanup struct { service SessionCleanupService cleanupInterval time.Duration ctx context.Context cancel context.CancelFunc wg sync.WaitGroup } // SessionCleanupConfig holds configuration for session cleanup. type SessionCleanupConfig struct { // CleanupInterval is how often to run cleanup. // Default: 5 minutes. CleanupInterval time.Duration } // DefaultSessionCleanupConfig returns sensible defaults. func DefaultSessionCleanupConfig() *SessionCleanupConfig { return &SessionCleanupConfig{ CleanupInterval: 5 * time.Minute, } } // NewSessionCleanup creates a new session cleanup worker. func NewSessionCleanup(service SessionCleanupService, cfg *SessionCleanupConfig) *SessionCleanup { if cfg == nil { cfg = DefaultSessionCleanupConfig() } ctx, cancel := context.WithCancel(context.Background()) return &SessionCleanup{ service: service, cleanupInterval: cfg.CleanupInterval, ctx: ctx, cancel: cancel, } } // Start begins the cleanup loop. func (c *SessionCleanup) Start() { log := logging.FromContext(c.ctx).WithWorker("session-cleanup") log.Info("session cleanup started", "cleanup_interval", c.cleanupInterval, ) c.wg.Add(1) go c.cleanupLoop() } // Stop gracefully shuts down the cleanup worker. func (c *SessionCleanup) Stop() { log := logging.FromContext(c.ctx).WithWorker("session-cleanup") log.Info("session cleanup stopping") c.cancel() c.wg.Wait() log.Info("session cleanup stopped") } // cleanupLoop runs periodic cleanup. func (c *SessionCleanup) 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 tears down expired sessions. func (c *SessionCleanup) runCleanup() { ctx, cancel := context.WithTimeout(c.ctx, TimeoutMaintenance) defer cancel() log := logging.FromContext(ctx).WithWorker("session-cleanup") cleaned, err := c.service.CleanupExpired(ctx) if err != nil { log.Error("failed to cleanup expired sessions", logging.FieldError, err, ) return } if cleaned > 0 { log.Info("cleaned up expired sessions", "count", cleaned, ) } }