fix(multiplayer): recover ambiguous agones allocations

This commit is contained in:
Josh Creek
2026-09-01 18:05:43 +01:00
parent b72a7cf843
commit bf396afcaf
6 changed files with 161 additions and 8 deletions
+68 -3
View File
@@ -85,15 +85,80 @@ type allocationResponse struct {
type gameServerListResponse struct {
Items []struct {
Metadata struct {
Name string `json:"name"`
Labels map[string]string `json:"labels"`
Name string `json:"name"`
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
} `json:"metadata"`
Status struct {
State string `json:"state"`
State string `json:"state"`
Address string `json:"address"`
Ports []struct {
Name string `json:"name"`
Port int `json:"port"`
} `json:"ports"`
} `json:"status"`
} `json:"items"`
}
// RecoverAllocation finds a provider-side allocation that may have completed
// before the durable allocation record was written. The allocation ID and
// compatibility tuple are checked together so a stale or forged provider
// object cannot be rebound to another match.
func (c Client) RecoverAllocation(ctx context.Context, request domain.AllocationRequest, now time.Time) (AllocatedServer, bool, error) {
if request.AllocationID == "" || request.MatchID == "" || now.IsZero() {
return AllocatedServer{}, false, domain.ErrAllocationInput
}
if c.HTTP == nil {
c.HTTP = http.DefaultClient
}
base, err := c.endpoint()
if err != nil {
return AllocatedServer{}, false, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/apis/agones.dev/v1/namespaces/"+url.PathEscape(c.Namespace)+"/gameservers", nil)
if err != nil {
return AllocatedServer{}, false, err
}
response, err := c.HTTP.Do(req)
if err != nil {
return AllocatedServer{}, false, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return AllocatedServer{}, false, fmt.Errorf("Agones GameServer recovery returned %s", response.Status)
}
var decoded gameServerListResponse
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&decoded); err != nil {
return AllocatedServer{}, false, fmt.Errorf("decode Agones recovery list: %w", err)
}
found := false
var recovered AllocatedServer
for _, item := range decoded.Items {
if item.Status.State != "Allocated" || item.Metadata.Annotations["cosmic-clash.io/allocation-id"] != request.AllocationID {
continue
}
if found {
return AllocatedServer{}, false, domain.ErrConflict
}
if item.Metadata.Name == "" || item.Status.Address == "" || strings.ContainsAny(item.Status.Address, " \t\r\n") {
return AllocatedServer{}, false, fmt.Errorf("Agones recovered GameServer has invalid identity or address")
}
if item.Metadata.Annotations["cosmic-clash.io/match-id"] != request.MatchID {
return AllocatedServer{}, false, domain.ErrConflict
}
if item.Metadata.Labels["cosmic-clash.io/region"] != request.Region || item.Metadata.Labels["cosmic-clash.io/build"] != request.Build || item.Metadata.Labels["cosmic-clash.io/protocol"] != strconv.Itoa(request.Protocol) || item.Metadata.Labels["cosmic-clash.io/transport"] != request.Transport {
return AllocatedServer{}, false, domain.ErrConflict
}
port, err := selectPort(item.Status.Ports)
if err != nil {
return AllocatedServer{}, false, err
}
recovered = AllocatedServer{Allocation: domain.Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: item.Metadata.Name, Region: request.Region, Build: request.Build, Protocol: request.Protocol, Transport: request.Transport, State: domain.ServerAllocated, AllocatedAt: now}, Endpoint: net.JoinHostPort(item.Status.Address, strconv.Itoa(port)), GameServer: item.Metadata.Name}
found = true
}
return recovered, found, nil
}
// ListReadyServers projects only Agones Ready GameServers into the durable
// allocator registry. Compatibility fields must be present as Fleet labels;
// malformed Ready objects fail closed instead of creating selectable capacity.