asobi_matchmaker (asobi v0.72.5)

View Source

The matchmaking queue. Players submit tickets with a mode and property constraints (add/2); a pluggable strategy groups compatible tickets, spawns a match, and pushes match.matched to each player. A single gen_server owns the queue and ticks it on an interval.

Summary

Types

mode_queue()

-type mode_queue() ::
          #{mode := binary(),
            waiting := pos_integer(),
            oldest_wait_ms := non_neg_integer(),
            average_wait_ms := non_neg_integer()}.

snapshot()

-type snapshot() ::
          #{sampled_at := integer() | null,
            age_ms := non_neg_integer() | null,
            waiting := non_neg_integer(),
            modes := [mode_queue()]}.

Functions

add(PlayerId, Params)

-spec add(binary(), map()) -> {ok, binary(), map()} | {error, queue_full}.

get_queue_stats()

-spec get_queue_stats() -> {ok, map()}.

get_ticket(TicketId)

-spec get_ticket(binary()) -> {ok, map()} | {error, not_found}.

get_ticket(PlayerId, TicketId)

-spec get_ticket(binary(), binary()) -> {ok, map()} | {error, not_found | not_owner}.

handle_call/3

-spec handle_call(term(), gen_server:from(), map()) -> {reply, term(), map()}.

handle_cast(Msg, State)

-spec handle_cast(term(), map()) -> {noreply, map()}.

handle_info/2

-spec handle_info(term(), map()) -> {noreply, map()}.

init/1

-spec init([]) -> {ok, map()}.

known_mode/1

-spec known_mode(term()) -> boolean().

remove(PlayerId, TicketId)

-spec remove(binary(), binary()) -> ok | {error, not_found | not_owner}.

snapshot()

-spec snapshot() -> snapshot().

The queue as an operator sees it: how many tickets are waiting, split by mode, and how long they have been waiting.

Read from an ETS table this process publishes to on each tick, not by calling this process. The matchmaker is a single gen_server that runs every strategy and spawns every match inside its own tick, so a gen_server:call from an HTTP handler queues behind that work: the read would be slowest exactly when the queue is deepest and an operator most needs to look at it. The other direction is worse - HTTP concurrency is unbounded, so a console polling once a second, times however many operators, lands in the mailbox of the hottest process in the system. A read plane must not be able to slow the game path down, at any request rate.

Publishing costs one ets:insert/2 per tick over a fold the tick has already paid for. A reader pays one ets:lookup/2 and sends no message at all.

Waits are derived at read time from the sampled submitted_at values, so they keep ageing correctly between ticks; only the counts are as old as age_ms, at most one tick_interval (1s by default). Before the first tick, or with no matchmaker running, the result is an empty queue rather than an error - null timestamps say which.

start_link()

-spec start_link() -> gen_server:start_ret().

terminate(Reason, State)

-spec terminate(term(), map()) -> ok.