diff --git a/game-server-hosting/server/internal/localproxy/allocation.go b/game-server-hosting/server/internal/localproxy/allocation.go new file mode 100644 index 0000000..96bbcb9 --- /dev/null +++ b/game-server-hosting/server/internal/localproxy/allocation.go @@ -0,0 +1,62 @@ +package localproxy + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/Unity-Technologies/unity-gaming-services-go-sdk/game-server-hosting/server/model" + "github.com/google/uuid" +) + +// PatchAllocation triggers the local proxy endpoint to patch this server allocation. +func (c *Client) PatchAllocation(ctx context.Context, allocationID string, args *model.PatchAllocationRequest) error { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + buf := bytes.NewBuffer(nil) + if err := json.NewEncoder(buf).Encode(args); err != nil { + return fmt.Errorf("error encoding args: %w", err) + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPatch, + fmt.Sprintf("%s/v1/servers/%d/allocations/%s", c.host, c.serverID, allocationID), + buf, + ) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + // Add a request ID - if we cannot generate a UUID for any reason, just populate an empty one. + requestID, err := uuid.NewUUID() + if err != nil { + requestID = uuid.UUID{} + } + + req.Header.Add("X-Request-ID", requestID.String()) + + var resp *http.Response + if resp, err = c.httpClient.Do(req); err != nil { + return fmt.Errorf("error making request: %w", err) + } + + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusNoContent { + body, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return NewUnexpectedResponseWithError(requestID.String(), resp.StatusCode, readErr) + } + return NewUnexpectedResponseWithBody(requestID.String(), resp.StatusCode, body) + } + + return nil +} diff --git a/game-server-hosting/server/internal/localproxy/allocation_test.go b/game-server-hosting/server/internal/localproxy/allocation_test.go new file mode 100644 index 0000000..49cc388 --- /dev/null +++ b/game-server-hosting/server/internal/localproxy/allocation_test.go @@ -0,0 +1,35 @@ +package localproxy + +import ( + "context" + "testing" + + "github.com/Unity-Technologies/unity-gaming-services-go-sdk/game-server-hosting/server/model" + "github.com/Unity-Technologies/unity-gaming-services-go-sdk/internal/localproxytest" + "github.com/stretchr/testify/require" +) + +func Test_PatchAllocation(t *testing.T) { + t.Parallel() + + proxy, err := localproxytest.NewLocalProxy() + require.NoError(t, err, "creating local proxy") + defer proxy.Close() + + chanError := make(chan error, 1) + + c, err := New(proxy.Host, 1, chanError) + require.NoError(t, err) + + args := &model.PatchAllocationRequest{ + Ready: true, + } + + ctx := context.Background() + alloc := "00000001-0000-0000-0000-000000000000" + + require.NoError(t, c.PatchAllocation(ctx, alloc, args), "patching allocation") + require.Contains(t, proxy.PatchAllocationRequests, alloc, "missing patch allocation request") + require.NotNil(t, proxy.PatchAllocationRequests[alloc], "nil patch allocation request") + require.Equal(t, true, proxy.PatchAllocationRequests[alloc].Ready, "unexpected ready value") +} diff --git a/game-server-hosting/server/model/allocation.go b/game-server-hosting/server/model/allocation.go new file mode 100644 index 0000000..4dcbf6b --- /dev/null +++ b/game-server-hosting/server/model/allocation.go @@ -0,0 +1,7 @@ +package model + +// PatchAllocationRequest defines the model for the request to patch a server allocation. +type PatchAllocationRequest struct { + // Ready is the ready state of the server. + Ready bool `json:"ready"` +} diff --git a/game-server-hosting/server/server.go b/game-server-hosting/server/server.go index 0ec0f44..a3993f0 100644 --- a/game-server-hosting/server/server.go +++ b/game-server-hosting/server/server.go @@ -110,6 +110,9 @@ var ( // ErrMetricOutOfBounds represents that the metric index provided will overflow the metrics buffer. ErrMetricOutOfBounds = errors.New("metrics index provided will overflow the metrics buffer") + + /// ErrNotAllocated represents that the server is not allocated. + ErrNotAllocated = errors.New("server is not allocated") ) // New creates a new instance of Server, denoting which type of server to use. @@ -296,6 +299,24 @@ func (s *Server) Release(ctx context.Context) error { return s.localProxyClient.ReleaseSelf(ctx) } +// ReadyForPlayers indicates the server is ready for players to join. +func (s *Server) ReadyForPlayers(ctx context.Context) error { + if ctx == nil { + return ErrNilContext + } + + allocationID := s.currentConfig.AllocatedUUID + if allocationID == "" { + return ErrNotAllocated + } + + patch := &model.PatchAllocationRequest{ + Ready: true, + } + + return s.localProxyClient.PatchAllocation(ctx, allocationID, patch) +} + // PlayerJoined indicates a new player has joined the server. func (s *Server) PlayerJoined() int32 { s.stateLock.Lock() diff --git a/game-server-hosting/server/server_test.go b/game-server-hosting/server/server_test.go index 6660114..227b7f6 100644 --- a/game-server-hosting/server/server_test.go +++ b/game-server-hosting/server/server_test.go @@ -412,3 +412,43 @@ func Test_SetMetric(t *testing.T) { require.NoError(t, s.Stop()) } + +func Test_ReadyForPlayers(t *testing.T) { + t.Parallel() + + ctx := context.Background() + dir := t.TempDir() + + queryEndpoint, err := getRandomPortAssignment() + require.NoError(t, err, "getting random port") + + proxy, err := localproxytest.NewLocalProxy() + require.NoError(t, err, "creating local proxy") + defer proxy.Close() + + alloc := "00000001-0000-0000-0000-000000000000" + port := strings.Split(queryEndpoint, ":")[1] + + data := []byte(fmt.Sprintf(`{ + "allocatedUUID": "%s", + "localProxyUrl": "%s", + "queryPort": "%s", + "queryType": "sqp", + "serverID": "1", + "serverLogDir": "%s" + }`, alloc, proxy.Host, port, filepath.Join(dir, "logs"))) + + configPath := filepath.Join(dir, "server.json") + require.NoError(t, os.WriteFile(configPath, data, 0o600), "writing config file") + + s, err := New(TypeAllocation, WithConfigPath(configPath)) + require.NoError(t, err, "making test server") + require.NotNil(t, s, "nil test server") + + require.NoError(t, s.Start(), "starting test server") + + require.NoError(t, s.ReadyForPlayers(ctx), "ready for players") + require.Contains(t, proxy.PatchAllocationRequests, alloc, "missing patch allocation request") + require.NotNil(t, proxy.PatchAllocationRequests[alloc], "nil patch allocation request") + require.Equal(t, true, proxy.PatchAllocationRequests[alloc].Ready, "unexpected ready value") +} diff --git a/internal/localproxytest/server.go b/internal/localproxytest/server.go index 3377bbb..c2bf35b 100644 --- a/internal/localproxytest/server.go +++ b/internal/localproxytest/server.go @@ -32,6 +32,10 @@ type MockLocalProxy struct { // HoldStatus is the status of a server hold, the response this mock uses. HoldStatus *model.HoldStatus + + // PatchAllocationRequests is a map of the last requests made to patch each + // allocation. This is used to verify the request body. + PatchAllocationRequests map[string]*model.PatchAllocationRequest } // NewLocalProxy sets up a new websocket server with centrifuge which accepts all connections and subscriptions. @@ -85,6 +89,8 @@ func NewLocalProxy() (*MockLocalProxy, error) { Held: true, } + patchAllocationRequests := make(map[string]*model.PatchAllocationRequest) + ws := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { // Satisfy the request for a connection to a centrifuge broker. @@ -124,17 +130,31 @@ func NewLocalProxy() (*MockLocalProxy, error) { default: w.WriteHeader(http.StatusMethodNotAllowed) } + + case "/v1/servers/1/allocations/00000001-0000-0000-0000-000000000000": + switch r.Method { + case http.MethodPatch: + req := &model.PatchAllocationRequest{} + _ = json.NewDecoder(r.Body).Decode(req) + patchAllocationRequests["00000001-0000-0000-0000-000000000000"] = req + w.WriteHeader(http.StatusNoContent) + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } } })) + ip = ws.URL return &MockLocalProxy{ - Server: ws, - Node: node, - Host: ws.URL, - JWT: token, - ReserveResponse: reserveResponse, - HoldStatus: holdStatus, + Server: ws, + Node: node, + Host: ws.URL, + JWT: token, + ReserveResponse: reserveResponse, + HoldStatus: holdStatus, + PatchAllocationRequests: patchAllocationRequests, }, nil }