feat: reconcile Agones allocations durably

This commit is contained in:
Josh Creek
2026-09-01 09:51:52 +01:00
parent 931e51a647
commit 0023bdab6e
6 changed files with 168 additions and 3 deletions
+60 -1
View File
@@ -37,6 +37,14 @@ const SelectAllocationSQL = `SELECT allocation_id, match_id, server_id, region,
protocol_version, transport, allocated_at, request_digest
FROM allocations WHERE allocation_id = $1`
const ProviderServerClaimSQL = `UPDATE game_servers SET state = 'ALLOCATED', updated_at = $6
WHERE server_id = $1 AND state = 'READY' AND region = $2 AND build = $3
AND protocol_version = $4 AND transport = $5
RETURNING server_id`
const ServerAllocationConflictSQL = `SELECT allocation_id FROM allocations
WHERE server_id = $1 FOR UPDATE`
func RegisterReadyServer(ctx context.Context, db *sql.DB, server domain.ReadyServer, now time.Time) error {
if db == nil || server.ServerID == "" || (server.Region != "EU" && server.Region != "NA") || server.Build == "" || server.Protocol <= 0 || (server.Transport != "enet" && server.Transport != "steam_sdr") || server.State != domain.ServerReady || now.IsZero() {
return fmt.Errorf("invalid ready server registration")
@@ -46,7 +54,7 @@ func RegisterReadyServer(ctx context.Context, db *sql.DB, server domain.ReadySer
}
func ClaimAllocation(ctx context.Context, db *sql.DB, request domain.AllocationRequest, now time.Time) (domain.Allocation, error) {
if db == nil || request.AllocationID == "" || request.MatchID == "" || (request.Region != "EU" && request.Region != "NA") || request.Build == "" || request.Protocol <= 0 || (request.Transport != "enet" && request.Transport != "steam_sdr") || now.IsZero() {
if !validAllocationInput(db, request, now) {
return domain.Allocation{}, domain.ErrAllocationInput
}
digest := allocationRequestDigest(request)
@@ -80,6 +88,57 @@ func ClaimAllocation(ctx context.Context, db *sql.DB, request domain.AllocationR
return allocation, err
}
// RecordProviderAllocation reconciles a provider-side Agones claim with the
// durable registry. It is deliberately separate from ClaimAllocation because
// Agones has already selected the server; no client-facing assignment may use
// the result until this exact tuple is durably recorded.
func RecordProviderAllocation(ctx context.Context, db *sql.DB, allocation domain.Allocation, now time.Time) (domain.Allocation, error) {
request := domain.AllocationRequest{AllocationID: allocation.AllocationID, MatchID: allocation.MatchID, Region: allocation.Region, Build: allocation.Build, Protocol: allocation.Protocol, Transport: allocation.Transport}
if !validAllocationInput(db, request, now) || allocation.State != domain.ServerAllocated || allocation.ServerID == "" {
return domain.Allocation{}, domain.ErrAllocationInput
}
digest := allocationRequestDigest(request)
var recorded domain.Allocation
err := RunSerializable(ctx, db, DefaultSerializableAttempts, func(ctx context.Context, tx *sql.Tx) error {
var prior domain.Allocation
var priorDigest []byte
err := tx.QueryRowContext(ctx, SelectAllocationSQL, allocation.AllocationID).Scan(&prior.AllocationID, &prior.MatchID, &prior.ServerID, &prior.Region, &prior.Build, &prior.Protocol, &prior.Transport, &prior.AllocatedAt, &priorDigest)
if err == nil {
if !bytes.Equal(priorDigest, digest[:]) || prior.ServerID != allocation.ServerID {
return domain.ErrConflict
}
recorded = prior
recorded.State = domain.ServerAllocated
return nil
}
if err != sql.ErrNoRows {
return err
}
var existing string
if err := tx.QueryRowContext(ctx, ServerAllocationConflictSQL, allocation.ServerID).Scan(&existing); err == nil {
return domain.ErrConflict
} else if err != sql.ErrNoRows {
return err
}
var serverID string
if err := tx.QueryRowContext(ctx, ProviderServerClaimSQL, allocation.ServerID, allocation.Region, allocation.Build, allocation.Protocol, allocation.Transport, now).Scan(&serverID); err != nil {
if err == sql.ErrNoRows {
return domain.ErrNoCapacity
}
return err
}
recorded = allocation
recorded.AllocatedAt = now
_, err = tx.ExecContext(ctx, InsertAllocationSQL, allocation.AllocationID, allocation.MatchID, serverID, allocation.Region, allocation.Build, allocation.Protocol, allocation.Transport, digest[:], now)
return err
})
return recorded, err
}
func validAllocationInput(db *sql.DB, request domain.AllocationRequest, now time.Time) bool {
return db != nil && request.AllocationID != "" && request.MatchID != "" && (request.Region == "EU" || request.Region == "NA") && request.Build != "" && request.Protocol > 0 && (request.Transport == "enet" || request.Transport == "steam_sdr") && !now.IsZero()
}
func allocationRequestDigest(request domain.AllocationRequest) [32]byte {
return sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%s\x00%s\x00%d\x00%s", request.AllocationID, request.MatchID, request.Region, request.Build, request.Protocol, request.Transport)))
}