- 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>
127 lines
3.2 KiB
Go
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
|
|
}
|