mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 19:33:44 +00:00
4cad0f0cce
Closes the second blocker named last commit. Re-traced the actual code path rather than trusting the earlier assumption: server_boot.gd verifies its mounted roster file synchronously in _ready(), before NetworkManager.host() runs and before ServerControl.set_process_ready is ever called -- so by the time the loopback /ready probe (and thus Agones Ready, and thus process-ready registration) succeeds, Godot has already verified its own roster. And the API's ASSIGNMENT_READY gate (AdvanceServerRegistrationSQL) checks only durable `assignments` rows server-side, nothing Godot reports. No new Godot-side state was needed -- the earlier 'needs Godot's own roster-verification state exposed' claim was overcautious and is corrected here. The supervisor now calls registerControlPlane(ctx, true) right after process-ready succeeds, with a bounded retry (default 5 attempts, 2s apart, both configurable) rather than a single attempt: the durable `assignments` rows the server-side gate checks may not have propagated by the first attempt, and that is expected, not fatal. Unlike a process-ready registration failure, a persistent assignment-ready failure does NOT kill the child -- the process is already legitimately listening and usable, and killing a healthy process over a lagging control-plane read would be actively harmful; it's logged to stderr instead. Covered by two tests: the full process-ready-then-assignment-ready sequence and body shapes, and a retry test that fails the assignment- ready call twice with 409 (simulating the real gate not yet satisfied) before succeeding on the third attempt, asserting Start() still succeeds and the child is never killed.
90 lines
3.7 KiB
Go
90 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/cosmic-clash/cosmic-clash/server/supervisor"
|
|
)
|
|
|
|
const usageText = `Usage: game-server-supervisor [options] -- <game-server-command> [args...]
|
|
|
|
The child command is started only after an allocated Agones endpoint has been
|
|
validated and, when configured, an explicit process-ready probe succeeds.
|
|
SIGTERM/SIGINT requests authenticated drain before the bounded grace deadline.
|
|
`
|
|
|
|
func main() {
|
|
args := os.Args[1:]
|
|
separator := -1
|
|
for i, arg := range args {
|
|
if arg == "--" {
|
|
separator = i
|
|
break
|
|
}
|
|
}
|
|
if separator < 0 || separator == len(args)-1 {
|
|
fmt.Fprint(os.Stderr, usageText)
|
|
os.Exit(2)
|
|
}
|
|
|
|
options := flag.NewFlagSet("game-server-supervisor", flag.ContinueOnError)
|
|
options.SetOutput(os.Stderr)
|
|
sdkBaseURL := options.String("sdk-base-url", "", "Agones SDK REST base URL; empty enables direct mode")
|
|
readyURL := options.String("ready-url", "", "explicit process-ready probe URL")
|
|
drainURL := options.String("drain-url", "", "loopback drain URL")
|
|
drainTokenEnv := options.String("drain-token-env", "COSMIC_CLASH_DRAIN_TOKEN", "environment variable containing the drain bearer token")
|
|
transport := options.String("transport", "enet", "enet or steam_sdr")
|
|
grace := options.Duration("drain-grace", supervisor.DefaultDrainGrace, "maximum graceful drain duration")
|
|
controlPlaneURL := options.String("control-plane-url", "", "matchmaking control-plane base URL; empty skips process-ready registration entirely")
|
|
workloadTokenPath := options.String("workload-token-path", "", "path to the projected workload service-account token, read fresh on every registration call")
|
|
serverIDEnv := options.String("server-id-env", "COSMIC_CLASH_SERVER_ID", "environment variable containing this GameServer's control-plane server ID (populate via the Kubernetes Downward API, fieldRef: metadata.name)")
|
|
matchIDEnv := options.String("match-id-env", "COSMIC_CLASH_MATCH_ID", "environment variable containing the allocated match ID; if unset/empty, falls back to the cosmic-clash.io/match-id annotation on the allocated GameServer")
|
|
protocolVersion := options.Int("protocol-version", 0, "protocol version reported at registration")
|
|
imageDigestEnv := options.String("image-digest-env", "COSMIC_CLASH_IMAGE_DIGEST", "environment variable containing this build's sha256 image digest")
|
|
assignmentReadyAttempts := options.Int("assignment-ready-attempts", 5, "retry attempts for assignment-ready registration after process-ready succeeds (a slow-to-propagate signed roster is not fatal)")
|
|
assignmentReadyBackoff := options.Duration("assignment-ready-backoff", 2*time.Second, "delay between assignment-ready retry attempts")
|
|
if err := options.Parse(args[:separator]); err != nil {
|
|
os.Exit(2)
|
|
}
|
|
|
|
token := ""
|
|
if *drainTokenEnv != "" {
|
|
token = os.Getenv(*drainTokenEnv)
|
|
}
|
|
s, err := supervisor.New(supervisor.Config{
|
|
Command: args[separator+1:],
|
|
SDKBaseURL: *sdkBaseURL,
|
|
ReadyURL: *readyURL,
|
|
DrainURL: *drainURL,
|
|
DrainToken: token,
|
|
Transport: *transport,
|
|
ReadyTimeout: 30 * time.Second,
|
|
|
|
ControlPlaneURL: *controlPlaneURL,
|
|
WorkloadTokenPath: *workloadTokenPath,
|
|
ServerID: os.Getenv(*serverIDEnv),
|
|
MatchID: os.Getenv(*matchIDEnv),
|
|
ProtocolVersion: *protocolVersion,
|
|
ImageDigest: os.Getenv(*imageDigestEnv),
|
|
|
|
AssignmentReadyAttempts: *assignmentReadyAttempts,
|
|
AssignmentReadyBackoff: *assignmentReadyBackoff,
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "game-server-supervisor: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
if err := s.Run(ctx, *grace); err != nil {
|
|
fmt.Fprintf(os.Stderr, "game-server-supervisor: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|