package main import ( "context" "database/sql" "flag" "fmt" "log" "os" "os/signal" "syscall" "time" "github.com/cosmic-clash/cosmic-clash/server/migrations" "github.com/cosmic-clash/cosmic-clash/server/store" _ "github.com/jackc/pgx/v5/stdlib" ) func main() { dsn := flag.String("dsn", os.Getenv("COSMIC_CLASH_POSTGRES_DSN"), "PostgreSQL connection string") migrationDir := flag.String("migrations", "migrations", "directory containing numbered SQL migrations") interval := flag.Duration("interval", time.Minute, "maintenance poll interval") initialConnectInterval := flag.Duration("initial-connect-interval", time.Second, "initial-connect reconciliation poll interval") batch := flag.Int("batch", 100, "maximum player rollovers per pass") stalledAllocationDeadline := flag.Duration("stalled-allocation-deadline", 2*time.Minute, "reclaim a match stuck in ALLOCATING/PROCESS_READY/ASSIGNMENT_READY (server crashed or was reclaimed before registering) after this long, requeuing every participant without penalty") stalledAllocationBatch := flag.Int("stalled-allocation-batch", 100, "maximum stalled matches reclaimed per pass") initialConnectBatch := flag.Int("initial-connect-batch", 100, "maximum pre-live matches evaluated per pass") flag.Parse() if *dsn == "" { fatalf("--dsn or COSMIC_CLASH_POSTGRES_DSN is required") } if *interval <= 0 || *initialConnectInterval <= 0 || *batch < 1 || *batch > 1000 { fatalf("invalid interval or batch") } if *stalledAllocationDeadline <= 0 || *stalledAllocationBatch < 1 || *stalledAllocationBatch > 1000 { fatalf("invalid stalled-allocation deadline or batch") } if *initialConnectBatch < 1 || *initialConnectBatch > 1000 { fatalf("invalid initial-connect batch") } db, err := sql.Open("pgx", *dsn) if err != nil { fatalf("open PostgreSQL: %v", err) } defer db.Close() startupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := db.PingContext(startupCtx); err != nil { fatalf("ping PostgreSQL: %v", err) } if err := migrations.Apply(startupCtx, db, *migrationDir); err != nil { fatalf("apply migrations: %v", err) } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() runGeneral := func(now time.Time) { count, err := store.RolloverDueSeasons(ctx, db, now, *batch) if err != nil { fatalf("season maintenance: %v", err) } if count > 0 { log.Printf("applied %d ranked season rollovers", count) } reclaimed, err := store.ExpireStalledAllocations(ctx, db, now, *stalledAllocationDeadline, *stalledAllocationBatch) if err != nil { fatalf("stalled-allocation maintenance: %v", err) } if reclaimed > 0 { log.Printf("reclaimed %d stalled allocations, requeuing their participants", reclaimed) } } runInitialConnect := func(now time.Time) { reconciled, err := store.ReconcileInitialConnect(ctx, db, now, *initialConnectBatch) if err != nil { fatalf("initial-connect maintenance: %v", err) } if reconciled > 0 { log.Printf("reconciled %d initial-connect outcomes", reconciled) } } runGeneral(time.Now().UTC()) runInitialConnect(time.Now().UTC()) generalTicker := time.NewTicker(*interval) initialConnectTicker := time.NewTicker(*initialConnectInterval) defer generalTicker.Stop() defer initialConnectTicker.Stop() for { select { case <-ctx.Done(): return case now := <-generalTicker.C: runGeneral(now.UTC()) case now := <-initialConnectTicker.C: runInitialConnect(now.UTC()) } } } func fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, "maintenance: "+format+"\n", args...) os.Exit(1) }