rdev/internal/worker/session_cleanup.go
jordan 9226454b85
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: label-based undeploy, GC reconciliation, checkout/sessions, pool status
- Add UndeployAll() using label selectors to clean up monorepo components
  on project deletion (replaces name-based Undeploy in DeleteProject and
  the direct undeploy handler)
- Add ResourceGC background worker that periodically finds K8s resources
  whose project label has no matching DB record, deletes after 1h safety
  window
- Widen deployer client type from *kubernetes.Clientset to
  kubernetes.Interface for testability
- UndeployAll accumulates errors via errors.Join instead of failing fast
- Add checkout/checkin sidecar dev flow: temporary git tokens, branch
  checkout, review on checkin with cleanup workers
- Add interactive sessions: pod binding, command execution, SSE streaming,
  ephemeral preview URLs with session cleanup workers
- Add GET /workers/pool endpoint for aggregate capacity and queue depth
- Add sessions:read and sessions:execute auth scopes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 19:11:28 -07:00

118 lines
2.6 KiB
Go

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,
)
}
}