feat(world): a session that calls the world raises instead of stalling it #49
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/session-tripwire"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The barrier is a synchronous receive, so a GenServer.call from a seated
session deadlocks it for the full 2500ms deadline every time it runs. The
process that could answer is the one blocked waiting for the caller, so it
is neither a race nor load-dependent.
Every other signal is absent or misleading. The call succeeds -- the
default call timeout is 5000ms against a 2500ms deadline -- so nothing
exits, nothing crashes and no supervisor notices; the blocking line is
still there next tick, so it repeats indefinitely; and because the tick
schedules its successor on its first line, a 2500ms tick leaves the next
one 1.9s overdue and it fires immediately. That catch-up burst advances
every walking player several steps into a ten-tile client render queue
that discards the oldest past nine, and since the client renders purely
from server deltas a discarded tile is a permanent offset with nothing in
the protocol to correct it. One blocking call leaves other players walking
through walls.
So the guard is on the caller's side, which is the only side with code
running: a check in handle_call/3 can never fire while the world sits in
the barrier's receive. Host.seat/7 marks its process -- the one
implementation the session, the test host and the bench share, all of
which run it in the owning process -- and World's call helper raises
World.BarrierViolation when it finds the mark.
join/2 is guarded and still works because it runs before seat/7, so the
mark goes up exactly when the process joins the expected set. leave/1 is
the one sanctioned exception and keeps its own helper: it raw-sends
{:session_leaving, index} first, so the barrier stops waiting before it
blocks.
The mark is a process dictionary entry rather than a Sessions lookup: it
costs one read on a path taken four ways in the whole codebase, so it runs
in every environment, and it travels with the process, so a call made deep
inside a content script trips it exactly as a direct one does.
Tests cover refusal, permission and the sanctioned exception -- and that
seat/7 actually arms it, asked from inside a real seated host, because a
guard nothing arms keeps passing forever while protecting nothing.
The barrier is a synchronous receive, so a GenServer.call from a seated session deadlocks it for the full 2500ms deadline every time it runs. The process that could answer is the one blocked waiting for the caller, so it is neither a race nor load-dependent. Every other signal is absent or misleading. The call succeeds -- the default call timeout is 5000ms against a 2500ms deadline -- so nothing exits, nothing crashes and no supervisor notices; the blocking line is still there next tick, so it repeats indefinitely; and because the tick schedules its successor on its first line, a 2500ms tick leaves the next one 1.9s overdue and it fires immediately. That catch-up burst advances every walking player several steps into a ten-tile client render queue that discards the oldest past nine, and since the client renders purely from server deltas a discarded tile is a permanent offset with nothing in the protocol to correct it. One blocking call leaves other players walking through walls. So the guard is on the caller's side, which is the only side with code running: a check in handle_call/3 can never fire while the world sits in the barrier's receive. Host.seat/7 marks its process -- the one implementation the session, the test host and the bench share, all of which run it in the owning process -- and World's call helper raises World.BarrierViolation when it finds the mark. join/2 is guarded and still works because it runs before seat/7, so the mark goes up exactly when the process joins the expected set. leave/1 is the one sanctioned exception and keeps its own helper: it raw-sends {:session_leaving, index} first, so the barrier stops waiting before it blocks. The mark is a process dictionary entry rather than a Sessions lookup: it costs one read on a path taken four ways in the whole codebase, so it runs in every environment, and it travels with the process, so a call made deep inside a content script trips it exactly as a direct one does. Tests cover refusal, permission and the sanctioned exception -- and that seat/7 actually arms it, asked from inside a real seated host, because a guard nothing arms keeps passing forever while protecting nothing.