mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-13 16:02:27 +00:00
fix(multiplayer): recover ambiguous agones allocations
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -154,3 +154,37 @@ func TestListReadyServersFailsClosedOnInvalidReadyCompatibility(t *testing.T) {
|
||||
t.Fatal("invalid Ready GameServer accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverAllocationFindsMatchingAllocatedGameServer(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || !strings.HasSuffix(r.URL.Path, "/gameservers") {
|
||||
t.Fatalf("request=%s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"items":[{"metadata":{"name":"gs-recovered","labels":{"cosmic-clash.io/region":"EU","cosmic-clash.io/build":"build-1","cosmic-clash.io/protocol":"1","cosmic-clash.io/transport":"enet"},"annotations":{"cosmic-clash.io/allocation-id":"allocation-1","cosmic-clash.io/match-id":"match-1"}},"status":{"state":"Allocated","address":"127.0.0.1","ports":[{"name":"default","port":31001}]}},{"metadata":{"name":"gs-other","annotations":{"cosmic-clash.io/allocation-id":"other"},"status":{"state":"Allocated"}}}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
recovered, found, err := (Client{BaseURL: server.URL, Namespace: "games", HTTP: server.Client()}).RecoverAllocation(context.Background(), request(), time.Unix(1000, 0))
|
||||
if err != nil || !found || recovered.GameServer != "gs-recovered" || recovered.Endpoint != "127.0.0.1:31001" || recovered.Allocation.ServerID != "gs-recovered" {
|
||||
t.Fatalf("recovered=%+v found=%t err=%v", recovered, found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverAllocationRejectsMismatchedBinding(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"items":[{"metadata":{"name":"gs-forged","labels":{"cosmic-clash.io/region":"NA","cosmic-clash.io/build":"build-1","cosmic-clash.io/protocol":"1","cosmic-clash.io/transport":"enet"},"annotations":{"cosmic-clash.io/allocation-id":"allocation-1","cosmic-clash.io/match-id":"other-match"}},"status":{"state":"Allocated","address":"127.0.0.1","ports":[{"name":"default","port":31001}]}}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
if _, found, err := (Client{BaseURL: server.URL, Namespace: "games", HTTP: server.Client()}).RecoverAllocation(context.Background(), request(), time.Unix(1000, 0)); err == nil || found {
|
||||
t.Fatalf("mismatched recovery accepted: found=%t err=%v", found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverAllocationRejectsDuplicateProviderMatches(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"items":[{"metadata":{"name":"gs-one","labels":{"cosmic-clash.io/region":"EU","cosmic-clash.io/build":"build-1","cosmic-clash.io/protocol":"1","cosmic-clash.io/transport":"enet"},"annotations":{"cosmic-clash.io/allocation-id":"allocation-1","cosmic-clash.io/match-id":"match-1"}},"status":{"state":"Allocated","address":"127.0.0.1","ports":[{"name":"default","port":31001}]}},{"metadata":{"name":"gs-two","labels":{"cosmic-clash.io/region":"EU","cosmic-clash.io/build":"build-1","cosmic-clash.io/protocol":"1","cosmic-clash.io/transport":"enet"},"annotations":{"cosmic-clash.io/allocation-id":"allocation-1","cosmic-clash.io/match-id":"match-1"}},"status":{"state":"Allocated","address":"127.0.0.1","ports":[{"name":"default","port":31002}]}}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
if _, found, err := (Client{BaseURL: server.URL, Namespace: "games", HTTP: server.Client()}).RecoverAllocation(context.Background(), request(), time.Unix(1000, 0)); err == nil || found {
|
||||
t.Fatalf("duplicate recovery accepted: found=%t err=%v", found, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user