rdev/internal/worker/checkout_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

119 lines
2.8 KiB
Go

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