ai-code-battle/cmd/acb-api/main.go
jedarden ea04f4debb style: apply gofmt alignment fixes across codebase
Tab/space alignment consistency from running gofmt on all packages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:40:33 -04:00

168 lines
5.2 KiB
Go

// Package main implements the AI Code Battle API server.
// It provides bot registration, job coordination, matchmaking,
// health checks, and rating updates. Connects to PostgreSQL
// for persistent storage and Valkey (Redis-compatible) for
// the job queue.
package main
import (
"context"
"database/sql"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/aicodebattle/acb/metrics"
"github.com/aicodebattle/acb/ratelimit"
_ "github.com/lib/pq"
"github.com/redis/go-redis/v9"
)
type Config struct {
ListenAddr string
DatabaseURL string
ValkeyAddr string
ValkeyPassword string
WorkerAPIKey string // API key workers use to submit results
EncryptionKey string // AES-256-GCM key for shared secret encryption
DiscordWebhook string // Discord webhook URL for alerts
SlackWebhook string // Slack webhook URL for alerts
MatchmakerSecs int
HealthCheckSecs int
ReaperSecs int
BotTimeoutSecs int
StaleJobMinutes int
MaxConsecFails int
SpamBlockList string // Comma-separated list of blocked terms (env: ACB_SPAM_BLOCK_LIST)
SpamMinLength int // Minimum feedback content length (env: ACB_SPAM_MIN_LENGTH)
}
func loadConfig() Config {
return Config{
ListenAddr: envOr("ACB_LISTEN_ADDR", ":8080"),
DatabaseURL: envOr("ACB_DATABASE_URL", "postgres://localhost:5432/acb?sslmode=disable"),
ValkeyAddr: envOr("ACB_VALKEY_ADDR", "localhost:6379"),
ValkeyPassword: os.Getenv("ACB_VALKEY_PASSWORD"),
WorkerAPIKey: os.Getenv("ACB_WORKER_API_KEY"),
EncryptionKey: os.Getenv("ACB_ENCRYPTION_KEY"),
DiscordWebhook: os.Getenv("ACB_DISCORD_WEBHOOK"),
SlackWebhook: os.Getenv("ACB_SLACK_WEBHOOK"),
MatchmakerSecs: envInt("ACB_MATCHMAKER_INTERVAL", 60),
HealthCheckSecs: envInt("ACB_HEALTHCHECK_INTERVAL", 900),
ReaperSecs: envInt("ACB_REAPER_INTERVAL", 300),
BotTimeoutSecs: envInt("ACB_BOT_TIMEOUT", 5),
StaleJobMinutes: envInt("ACB_STALE_JOB_MINUTES", 15),
MaxConsecFails: envInt("ACB_MAX_CONSEC_FAILS", 3),
SpamBlockList: os.Getenv("ACB_SPAM_BLOCK_LIST"),
SpamMinLength: envInt("ACB_SPAM_MIN_LENGTH", 10),
}
}
func main() {
cfg := loadConfig()
db, err := sql.Open("postgres", cfg.DatabaseURL)
if err != nil {
log.Fatalf("failed to open database: %v", err)
}
defer db.Close()
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
rdb := redis.NewClient(&redis.Options{
Addr: cfg.ValkeyAddr,
Password: cfg.ValkeyPassword,
})
defer rdb.Close()
srv := &Server{
cfg: cfg,
db: db,
rdb: rdb,
regLimiter: ratelimit.NewLimiter(5, 5.0/3600), // 5/hour per IP
feedbackLtr: ratelimit.NewLimiter(20, 20.0/3600), // 20/hour per IP
predictLtr: ratelimit.NewLimiter(60, 60.0/3600), // 60/hour per IP
submitLtr: ratelimit.NewLimiter(5, 5.0/86400), // 5/day per key
enrichLtr: ratelimit.NewLimiter(5, 5.0/86400), // 5/day per bot
voteLtr: ratelimit.NewLimiter(10, 10.0/3600), // 10/hour per IP
}
// Initialize spam filter with configurable block-list
var blockList []string
if cfg.SpamBlockList != "" {
blockList = strings.Split(cfg.SpamBlockList, ",")
for i := range blockList {
blockList[i] = strings.TrimSpace(blockList[i])
}
}
srv.spamFilter = NewSpamFilter(blockList, cfg.SpamMinLength)
log.Printf("[SPAMFILTER] initialized with %d blocked terms, min length %d",
srv.spamFilter.BlockedCount(), cfg.SpamMinLength)
// Periodically purge stale rate-limit buckets (every 10 min)
go func() {
ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()
for range ticker.C {
srv.regLimiter.Cleanup(time.Hour)
srv.feedbackLtr.Cleanup(time.Hour)
srv.predictLtr.Cleanup(time.Hour)
srv.submitLtr.Cleanup(24 * time.Hour)
srv.enrichLtr.Cleanup(24 * time.Hour)
srv.voteLtr.Cleanup(time.Hour)
}
}()
mux := http.NewServeMux()
srv.RegisterRoutes(mux)
// Start internal metrics server (separate port for Prometheus scraping)
metricsSrv := metrics.StartServer()
defer metricsSrv.Close()
httpSrv := &http.Server{
Addr: cfg.ListenAddr,
Handler: metrics.HTTPMiddleware(mux),
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Note: Background tickers (matchmaker, health-checker, stale-reaper) are now
// handled by the separate acb-matchmaker deployment per plan §12 Phase 4.
// This API server only handles HTTP endpoints for bot registration, job
// coordination, and bot status.
_ = ctx // ctx no longer needed since tickers moved to acb-matchmaker
// Graceful shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
log.Printf("acb-api listening on %s", cfg.ListenAddr)
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("http server error: %v", err)
}
}()
<-sigCh
log.Println("shutting down...")
cancel()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
if err := httpSrv.Shutdown(shutdownCtx); err != nil {
log.Printf("http shutdown error: %v", err)
}
log.Println("shutdown complete")
}