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") batch := flag.Int("batch", 100, "maximum player rollovers per pass") flag.Parse() if *dsn == "" { fatalf("--dsn or COSMIC_CLASH_POSTGRES_DSN is required") } if *interval <= 0 || *batch < 1 || *batch > 1000 { fatalf("invalid interval or 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() for { count, err := store.RolloverDueSeasons(ctx, db, time.Now().UTC(), *batch) if err != nil { fatalf("season maintenance: %v", err) } if count > 0 { log.Printf("applied %d ranked season rollovers", count) } timer := time.NewTimer(*interval) select { case <-ctx.Done(): timer.Stop() return case <-timer.C: } } } func fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, "maintenance: "+format+"\n", args...) os.Exit(1) }