mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
e5c4e0b89a
Same pattern and same discovery method as the ResultSubmitter fix: store.PostgresSessions already implements SessionIssuer.Issue (used by steamSession() to mint sessions) as well as SessionBackend.Authenticate (used to verify them), and was already wired for the latter -- but not the former, so /v1/session/steam always 503'd with auth_unavailable even before considering whether SteamLogin (the real, still-correctly- unwired Steam blocker) was available. Wire it: same struct value, second field. Not independently visible via a black-box HTTP test yet -- SteamLogin still nil means the handler's first guard clause still 503s before ever reaching SessionIssuer, so the observable symptom is unchanged until Steam access exists. Verified by reading the handler's actual branch order, not by a test that would currently pass for the wrong reason.
122 lines
4.1 KiB
Go
122 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/cosmic-clash/cosmic-clash/server/api"
|
|
"github.com/cosmic-clash/cosmic-clash/server/migrations"
|
|
"github.com/cosmic-clash/cosmic-clash/server/observability"
|
|
"github.com/cosmic-clash/cosmic-clash/server/store"
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
func main() {
|
|
listen := flag.String("listen", ":8080", "HTTP listen address")
|
|
role := flag.String("role", "api", "control-plane role; currently api")
|
|
dsn := flag.String("dsn", os.Getenv("COSMIC_CLASH_POSTGRES_DSN"), "PostgreSQL connection string")
|
|
migrationDir := flag.String("migrations", "migrations", "directory containing numbered SQL migrations")
|
|
redisAddr := flag.String("redis-addr", os.Getenv("COSMIC_CLASH_REDIS_ADDR"), "optional Redis address for the candidate projection")
|
|
redisPrefix := flag.String("redis-prefix", envOrDefault("COSMIC_CLASH_REDIS_PREFIX", "cosmic-clash"), "Redis key prefix")
|
|
redisTTL := flag.Duration("redis-ttl", 60*time.Second, "TTL for transient candidate projection entries")
|
|
flag.Parse()
|
|
if *role != "api" {
|
|
fatalf("unsupported role %q (only api is implemented)", *role)
|
|
}
|
|
if *dsn == "" {
|
|
fatalf("--dsn or COSMIC_CLASH_POSTGRES_DSN is required")
|
|
}
|
|
if *redisTTL <= 0 {
|
|
fatalf("--redis-ttl must be positive")
|
|
}
|
|
db, err := sql.Open("pgx", *dsn)
|
|
if err != nil {
|
|
fatalf("open PostgreSQL: %v", err)
|
|
}
|
|
defer db.Close()
|
|
startupCtx, startupCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer startupCancel()
|
|
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)
|
|
}
|
|
var candidateIndex api.CandidateIndex
|
|
var redisClient *redis.Client
|
|
if *redisAddr != "" {
|
|
redisClient = redis.NewClient(&redis.Options{Addr: *redisAddr})
|
|
defer redisClient.Close()
|
|
candidateIndex = store.RedisCandidateIndex{Client: redisClient, Prefix: *redisPrefix, TTL: *redisTTL}
|
|
}
|
|
server := &http.Server{Addr: *listen, Handler: newAPIHandler(db, candidateIndex), ReadHeaderTimeout: 5 * time.Second}
|
|
serveErr := make(chan error, 1)
|
|
go func() { serveErr <- server.ListenAndServe() }()
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
select {
|
|
case err := <-serveErr:
|
|
if err != nil && err != http.ErrServerClosed {
|
|
fatalf("serve API: %v", err)
|
|
}
|
|
case <-ctx.Done():
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer shutdownCancel()
|
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
|
fatalf("shutdown API: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newAPIHandler(db *sql.DB, indexes ...api.CandidateIndex) http.Handler {
|
|
var candidateIndex api.CandidateIndex
|
|
if len(indexes) > 0 {
|
|
candidateIndex = indexes[0]
|
|
}
|
|
return (&api.Service{
|
|
SessionBackend: store.PostgresSessions{DB: db},
|
|
SessionIssuer: store.PostgresSessions{DB: db},
|
|
QueueBackend: store.PostgresQueue{DB: db},
|
|
ProposalBackend: api.ProposalProviderFromStore(db),
|
|
ProposalPromoter: api.ProposalPromoterFromStore(db),
|
|
ServerRegistrar: api.ServerRegistrarFromStore(db),
|
|
ResultSubmitter: store.PostgresResults{DB: db},
|
|
Assignment: api.AssignmentProviderFromStore(db),
|
|
CandidateIndex: candidateIndex,
|
|
ProbeRecorder: store.PostgresQueue{DB: db},
|
|
Now: func() time.Time { return time.Now().UTC() },
|
|
Log: logEvent,
|
|
}).Handler()
|
|
}
|
|
|
|
// logEvent writes one credential-safe structured event per line to stderr.
|
|
// Best-effort: a logging failure must never fail or block the request it
|
|
// describes, so encode errors are swallowed rather than surfaced.
|
|
func logEvent(event observability.Event) {
|
|
payload, err := observability.Encode(event)
|
|
if err != nil {
|
|
return
|
|
}
|
|
fmt.Fprintln(os.Stderr, string(payload))
|
|
}
|
|
|
|
func envOrDefault(name, fallback string) string {
|
|
if value := os.Getenv(name); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func fatalf(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "control-plane: "+format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|