From 991b7179d8235bcc4ab513e61ae85ebb136d1272 Mon Sep 17 00:00:00 2001 From: Lexon <44340857+L-e-x-o-n@users.noreply.github.com> Date: Sun, 9 Feb 2025 15:06:03 +0100 Subject: [PATCH] Tachyon system/serverStats --- lib/teiserver/player.ex | 16 ++++--- lib/teiserver/player/session_registry.ex | 6 ++- lib/teiserver/player/tachyon_handler.ex | 6 +++ .../schema/system/serverStats/request.json | 17 +++++++ .../schema/system/serverStats/response.json | 47 +++++++++++++++++++ test/support/tachyon.ex | 23 ++++++++- test/teiserver_web/tachyon/system_test.exs | 41 ++++++++++++++++ 7 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 priv/tachyon/schema/system/serverStats/request.json create mode 100644 priv/tachyon/schema/system/serverStats/response.json create mode 100644 test/teiserver_web/tachyon/system_test.exs diff --git a/lib/teiserver/player.ex b/lib/teiserver/player.ex index b10c782c4..c1e2fc7c7 100644 --- a/lib/teiserver/player.ex +++ b/lib/teiserver/player.ex @@ -12,25 +12,27 @@ defmodule Teiserver.Player do alias Teiserver.Data.Types, as: T alias Teiserver.{Player, Matchmaking} + @doc """ + Returns the pid of the session registered with a given user id + """ + @spec lookup_session(T.userid()) :: pid() | nil + defdelegate lookup_session(user_id), to: Player.SessionRegistry, as: :lookup + @doc """ Returns the pid of the connection registered with a given user id """ @spec lookup_connection(T.userid()) :: pid() | nil - def lookup_connection(user_id) do - Player.Registry.lookup(user_id) - end + defdelegate lookup_connection(user_id), to: Player.Registry, as: :lookup @spec connection_via_tuple(T.userid()) :: GenServer.name() - def connection_via_tuple(user_id) do - Player.Registry.via_tuple(user_id) - end + defdelegate connection_via_tuple(user_id), to: Player.Registry, as: :via_tuple @doc """ To be used when a process is interested in the presence of a given player. """ @spec monitor_session(T.userid()) :: reference() | nil def monitor_session(user_id) do - pid = Player.SessionRegistry.lookup(user_id) + pid = lookup_connection(user_id) if is_nil(pid) do nil diff --git a/lib/teiserver/player/session_registry.ex b/lib/teiserver/player/session_registry.ex index 470aa4c58..9f25876f3 100644 --- a/lib/teiserver/player/session_registry.ex +++ b/lib/teiserver/player/session_registry.ex @@ -10,7 +10,7 @@ defmodule Teiserver.Player.SessionRegistry do end @doc """ - how to reach a given session + How to reach a given session """ @spec via_tuple(T.userid()) :: GenServer.name() def via_tuple(user_id) do @@ -35,4 +35,8 @@ defmodule Teiserver.Player.SessionRegistry do start: {__MODULE__, :start_link, []} ) end + + def count() do + Horde.Registry.count(__MODULE__) + end end diff --git a/lib/teiserver/player/tachyon_handler.ex b/lib/teiserver/player/tachyon_handler.ex index 01f549dee..5877e0c35 100644 --- a/lib/teiserver/player/tachyon_handler.ex +++ b/lib/teiserver/player/tachyon_handler.ex @@ -140,6 +140,12 @@ defmodule Teiserver.Player.TachyonHandler do {:stop, :normal, state} end + def handle_command("system/serverStats" = cmd_id, "request", _message_id, _message, state) do + user_count = Teiserver.Player.SessionRegistry.count() + + {:response, cmd_id, %{userCount: user_count}, state} + end + def handle_command("matchmaking/list" = cmd_id, "request", message_id, _message, state) do queues = Matchmaking.list_queues() diff --git a/priv/tachyon/schema/system/serverStats/request.json b/priv/tachyon/schema/system/serverStats/request.json new file mode 100644 index 000000000..e40feafa8 --- /dev/null +++ b/priv/tachyon/schema/system/serverStats/request.json @@ -0,0 +1,17 @@ +{ + "$id": "https://schema.beyondallreason.dev/tachyon/system/serverStats/request.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SystemServerStatsRequest", + "tachyon": { + "source": "user", + "target": "server", + "scopes": ["tachyon.lobby"] + }, + "type": "object", + "properties": { + "type": { "const": "request" }, + "messageId": { "type": "string" }, + "commandId": { "const": "system/serverStats" } + }, + "required": ["type", "messageId", "commandId"] +} \ No newline at end of file diff --git a/priv/tachyon/schema/system/serverStats/response.json b/priv/tachyon/schema/system/serverStats/response.json new file mode 100644 index 000000000..df6e6dd42 --- /dev/null +++ b/priv/tachyon/schema/system/serverStats/response.json @@ -0,0 +1,47 @@ +{ + "title": "SystemServerStatsResponse", + "tachyon": { + "source": "server", + "target": "user", + "scopes": ["tachyon.lobby"] + }, + "anyOf": [ + { + "title": "SystemServerStatsOkResponse", + "type": "object", + "properties": { + "type": { "const": "response" }, + "messageId": { "type": "string" }, + "commandId": { "const": "system/serverStats" }, + "status": { "const": "success" }, + "data": { + "title": "SystemServerStatsOkResponseData", + "type": "object", + "properties": { "userCount": { "type": "integer" } }, + "required": ["userCount"] + } + }, + "required": ["type", "messageId", "commandId", "status", "data"] + }, + { + "title": "SystemServerStatsFailResponse", + "type": "object", + "properties": { + "type": { "const": "response" }, + "messageId": { "type": "string" }, + "commandId": { "const": "system/serverStats" }, + "status": { "const": "failed" }, + "reason": { + "enum": [ + "internal_error", + "unauthorized", + "invalid_request", + "command_unimplemented" + ] + }, + "details": { "type": "string" } + }, + "required": ["type", "messageId", "commandId", "status", "reason"] + } + ] +} diff --git a/test/support/tachyon.ex b/test/support/tachyon.ex index 02c5e9e48..5b803b827 100644 --- a/test/support/tachyon.ex +++ b/test/support/tachyon.ex @@ -1,6 +1,7 @@ defmodule Teiserver.Support.Tachyon do alias WebsocketSyncClient, as: WSC alias Teiserver.OAuthFixtures + alias Teiserver.Player def setup_client(_context), do: setup_client() @@ -29,7 +30,20 @@ defmodule Teiserver.Support.Tachyon do {:ok, _user_updated} = recv_message(client) end - ExUnit.Callbacks.on_exit(fn -> WSC.disconnect(client) end) + ExUnit.Callbacks.on_exit(fn -> + WSC.disconnect(client) + + case Player.lookup_session(token.owner_id) do + nil -> :ok + pid -> Process.exit(pid, :test_cleanup) + end + + poll_until( + fn -> Player.lookup_session(token.owner_id) end, + fn x -> is_nil(x) end + ) + end) + client end @@ -226,6 +240,13 @@ defmodule Teiserver.Support.Tachyon do resp end + def server_stats!(client) do + req = request("system/serverStats") + :ok = WSC.send_message(client, {:text, req |> Jason.encode!()}) + {:ok, resp} = recv_message(client) + resp + end + @doc """ Run the given function `f` until `pred` returns true on its result. Waits `wait` ms between each tries. Raise an error if `pred` returns false diff --git a/test/teiserver_web/tachyon/system_test.exs b/test/teiserver_web/tachyon/system_test.exs new file mode 100644 index 000000000..f41cd5597 --- /dev/null +++ b/test/teiserver_web/tachyon/system_test.exs @@ -0,0 +1,41 @@ +defmodule TeiserverWeb.Tachyon.SystemTest do + use TeiserverWeb.ConnCase + alias Teiserver.Support.Tachyon + alias Teiserver.OAuthFixtures + + describe "server stats" do + defp setup_app(_context) do + owner = Central.Helpers.GeneralTestLib.make_user(%{"data" => %{"roles" => ["Verified"]}}) + + app = + OAuthFixtures.app_attrs(owner.id) + |> Map.put(:uid, UUID.uuid4()) + |> OAuthFixtures.create_app() + + {:ok, app: app} + end + + defp setup_user(app) do + user = Central.Helpers.GeneralTestLib.make_user(%{"data" => %{"roles" => ["Verified"]}}) + token = OAuthFixtures.token_attrs(user.id, app) |> OAuthFixtures.create_token() + client = Tachyon.connect(token) + {:ok, %{user: user, token: token, client: client}} + end + + setup [:setup_app] + + test "works", %{app: app} do + {:ok, %{client: client1}} = setup_user(app) + {:ok, %{client: client2}} = setup_user(app) + + assert %{"data" => %{"userCount" => 2}} = Tachyon.server_stats!(client1) + assert %{"data" => %{"userCount" => 2}} = Tachyon.server_stats!(client2) + + Tachyon.disconnect!(client2) + + Tachyon.poll_until(fn -> Tachyon.server_stats!(client1) end, fn data -> + data["data"]["userCount"] == 1 + end) + end + end +end