asobi_world_lobby_server (asobi v0.84.0)
View SourceSingle point of serialization for asobi_world_lobby:find_or_create/1.
The naive find_or_create implementation has a TOCTOU race: two
clients calling at the same time both see list_worlds(...) = []
because neither has finished create_world/1 yet, and both end up
spawning a new world for the same mode. Customer-visible symptom:
two players opening barrow at the same instant land in different
hub worlds and never see each other.
This gen_server forces all find_or_create calls through a single
process. The handler runs the existing list_worlds + create_world
sequence atomically with respect to other callers, so the second
caller sees the world the first one just spawned.
Calls are sequential, but create_world/1 is the slowest step and
takes <100ms in practice; for the small number of distinct modes a
typical deployment supports, the queue stays empty.
It also owns the protected ETS table backing the cached discovery
listings for both worlds and matches. Callers read the table directly
(protected reads); only this server writes, via cache_listing/3.
Summary
Functions
Store a computed listing against a bounded key with an absolute expiry. Async: the caller keeps the value it already computed; this only seeds the shared cache for the next reader.
Atomic version of asobi_world_lobby:find_or_create/1. Public
callers should always go through this — calling the inner function
directly is racy.
Functions
-spec cache_listing({module(), boolean()} | {module(), boolean(), boolean() | undefined}, [map()], integer()) -> ok.
Store a computed listing against a bounded key with an absolute expiry. Async: the caller keeps the value it already computed; this only seeds the shared cache for the next reader.
The key is {ServerMod, HasCapacity} for worlds and
{ServerMod, HasCapacity, Joinable} for matches - ten keys total across the
two. Every element after the module has to be a bounded, non-client-controlled
value: keying on mode would let a client cycle distinct modes to miss on
every request and grow the table without bound. The guard enforces that shape
rather than trusting the caller, which is why it is not simply is_tuple/1.
Atomic version of asobi_world_lobby:find_or_create/1. Public
callers should always go through this — calling the inner function
directly is racy.
-spec handle_call(term(), gen_server:from(), map()) -> {reply, term(), map()}.
-spec init([]) -> {ok, #{}}.
-spec start_link() -> gen_server:start_ret().