rdev/internal/worker/operation_cleanup.go
jordan c280a92012 feat: add operations audit system and template improvements
Operations Audit (new feature):
- Add Operation domain model with status tracking (pending, running, completed, failed, cancelled)
- Add OperationRepository with PostgreSQL implementation
- Add OperationService for CRUD and lifecycle management
- Add operations handlers (list, get, cancel endpoints)
- Add migration 015_operations.sql for operations table
- Add operation cleanup worker for stale operation handling
- Add ErrOperationNotFound to domain errors

Template Improvements:
- Add CLAUDE.md configuration files to astro-landing, default, and go-api templates
- Fix PORT template variable usage in nginx configs for app templates
- Add replace directives for local pkg module in Go templates
- Simplify Go service/worker Dockerfiles for workspace builds
- Fix TypeScript error in logger template

Other:
- Refactor landing-test.sh cookbook script
- Update CLAUDE.md version reference

Note: Some files exceed 500-line limit (pre-existing debt + new feature)
- component.go: 550 lines (unchanged, pre-existing)
- main.go: 522 lines (added operations wiring)
- operation_repo.go: 569 lines (new, needs splitting)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 19:08:57 -07:00

126 lines
2.9 KiB
Go

package worker
import (
"context"
"log/slog"
"sync"
"time"
"github.com/orchard9/rdev/internal/port"
)
// OperationCleanup runs periodic cleanup of old operations.
// Operations older than the retention period (default 30 days) are deleted.
type OperationCleanup struct {
repo port.OperationRepository
logger *slog.Logger
retentionPeriod time.Duration
cleanupInterval time.Duration
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
// OperationCleanupConfig holds configuration for operation cleanup.
type OperationCleanupConfig struct {
// RetentionPeriod is how long to keep operations.
// Default: 30 days.
RetentionPeriod time.Duration
// CleanupInterval is how often to run cleanup.
// Default: 1 hour.
CleanupInterval time.Duration
Logger *slog.Logger
}
// DefaultOperationCleanupConfig returns sensible defaults.
func DefaultOperationCleanupConfig() *OperationCleanupConfig {
return &OperationCleanupConfig{
RetentionPeriod: 30 * 24 * time.Hour, // 30 days
CleanupInterval: 1 * time.Hour,
Logger: slog.Default(),
}
}
// NewOperationCleanup creates a new operation cleanup worker.
func NewOperationCleanup(repo port.OperationRepository, cfg *OperationCleanupConfig) *OperationCleanup {
if cfg == nil {
cfg = DefaultOperationCleanupConfig()
}
ctx, cancel := context.WithCancel(context.Background())
return &OperationCleanup{
repo: repo,
logger: cfg.Logger.With("component", "operation-cleanup"),
retentionPeriod: cfg.RetentionPeriod,
cleanupInterval: cfg.CleanupInterval,
ctx: ctx,
cancel: cancel,
}
}
// Start begins the cleanup loop.
func (c *OperationCleanup) Start() {
c.logger.Info("operation cleanup started",
"retention_period", c.retentionPeriod,
"cleanup_interval", c.cleanupInterval,
)
c.wg.Add(1)
go c.cleanupLoop()
}
// Stop gracefully shuts down the cleanup worker.
func (c *OperationCleanup) Stop() {
c.logger.Info("operation cleanup stopping")
c.cancel()
c.wg.Wait()
c.logger.Info("operation cleanup stopped")
}
// cleanupLoop runs periodic cleanup.
func (c *OperationCleanup) 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 deletes operations older than the retention period.
func (c *OperationCleanup) runCleanup() {
ctx, cancel := context.WithTimeout(c.ctx, 30*time.Second)
defer cancel()
cutoff := time.Now().Add(-c.retentionPeriod)
deleted, err := c.repo.DeleteOlderThan(ctx, cutoff)
if err != nil {
c.logger.Error("failed to cleanup old operations",
"error", err,
"cutoff", cutoff,
)
return
}
if deleted > 0 {
c.logger.Info("cleaned up old operations",
"deleted", deleted,
"cutoff", cutoff,
)
}
}