ai-code-battle/cmd/acb-enrichment/main.go
jedarden a22b0b6aa3 feat(enrichment): align config with other services for K8s deployment
- Update Config struct to use individual postgres connection components (ACB_POSTGRES_HOST, ACB_POSTGRES_PORT, etc.) instead of ACB_DATABASE_URL
- Add DatabaseURL() method to build connection string from components
- This matches the pattern used by acb-index-builder and other services

Closes: bf-1ghm

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 01:44:59 -04:00

127 lines
3.2 KiB
Go

// Package main implements acb-enrichment, a service that generates AI commentary
// for featured AI Code Battle matches. It polls for matches without commentary,
// downloads replays from B2/R2, generates turn-by-turn narrative highlights via
// LLM, and stores results back to R2.
package main
import (
"context"
"database/sql"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"github.com/aicodebattle/acb/metrics"
_ "github.com/lib/pq"
)
func main() {
// Setup structured logging
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
cfg := LoadConfig()
// Connect to PostgreSQL
db, err := sql.Open("postgres", cfg.DatabaseURL())
if err != nil {
slog.Error("Failed to connect to PostgreSQL", "error", err)
os.Exit(1)
}
defer db.Close()
// Verify connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := db.PingContext(ctx); err != nil {
cancel()
slog.Error("Failed to ping PostgreSQL", "error", err)
os.Exit(1)
}
cancel()
slog.Info("Connected to PostgreSQL", "database", cfg.DatabaseName)
// Create enrichment service
svc := NewEnrichmentService(db, cfg)
// Verify storage configuration
if err := svc.CheckStorage(ctx); err != nil {
slog.Error("Storage check failed", "error", err)
os.Exit(1)
}
// Verify LLM configuration
if err := svc.CheckLLM(ctx); err != nil {
slog.Error("LLM check failed", "error", err)
os.Exit(1)
}
// Start internal metrics server
metricsSrv := metrics.StartServer()
defer metricsSrv.Close()
// Handle graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
startTime := time.Now()
cycleCount := 0
// Main enrichment loop
for {
// Check lifetime
if time.Since(startTime) > cfg.MaxLifetime {
slog.Info("Max lifetime reached, exiting", "lifetime", cfg.MaxLifetime)
os.Exit(0)
}
// Check for shutdown signal
select {
case sig := <-sigChan:
slog.Info("Received signal, shutting down", "signal", sig)
os.Exit(0)
default:
}
// Run enrichment cycle
cycleCount++
slog.Info("Starting enrichment cycle", "count", cycleCount)
cycleStart := time.Now()
cycleCtx, cycleCancel := context.WithTimeout(context.Background(), cfg.CycleTimeout)
results, err := svc.RunCycle(cycleCtx)
cycleCancel()
if err != nil {
slog.Error("Enrichment cycle failed", "error", err)
metrics.EnrichmentCycles.WithLabelValues("error").Inc()
} else {
slog.Info("Enrichment cycle completed",
"processed", results.Processed,
"enriched", results.Enriched,
"skipped", results.Skipped,
"failed", results.Failed,
)
metrics.EnrichmentCycles.WithLabelValues("success").Inc()
metrics.EnrichmentProcessed.Add(float64(results.Processed))
metrics.EnrichmentGenerated.Add(float64(results.Enriched))
}
metrics.EnrichmentCycleDuration.Observe(time.Since(cycleStart).Seconds())
// Sleep until next cycle
slog.Info("Sleeping until next enrichment cycle", "duration", cfg.CycleInterval)
time.Sleep(cfg.CycleInterval)
}
}
// CycleResults holds statistics from a single enrichment cycle.
type CycleResults struct {
Processed int
Enriched int
Skipped int
Failed int
}