One phase of a step: ask every module the same kind of question, and wait.
Monty's step is a sequence of barriers — all percepts, then all learning
module steps, then all votes — so the barrier is the unit of concurrency
here. Callers pass a list of {id, fun} pairs, where fun makes one call
through a module server's public API; the barrier does not know about
GenServers, mailboxes or payload shapes.
Every request starts before any of them is awaited, so a module that is slow does not hold up the modules behind it — that is Monty's round. Replies come back keyed by id, and a request that misses its deadline is reported rather than waited for, which is what makes an opportunistic vote round possible.
Each request runs in a process of its own, because a deadline needs one: a
call that will not come back has to be killed, and only its own process can
be. That process is the whole mechanism — spawned, monitored, and then its
reply or its death is received. Task.async_stream/3 does the same work
through a stream, a monitor per element and a :kill_task path, and measured
against it on this machine (paired, step by step, both under the same load)
the round below is the cheaper of the two; a step is four rounds and a run is
thousands of steps, so the round is worth keeping small.
A round of one request with no deadline has nothing to kill and no
concurrency to lose, so it runs the request where it was called. The process
is otherwise pure cost: spawn_monitor copies the request's environment into
the new one, and a request closure usually is the payload — a sensor request
closes over the observations, 3.2 MB a step. Measured here, seven spawns
carrying a 400 000-word term cost 83 ms against 32 µs carrying nothing, and
that copy was half of a model step's time, for a deadline the model never sets
(call_timeout: :infinity, Monty.Studio.Setup). A round of several requests
keeps a process each, deadline or none, because starting together is the
round's point; the payload is what to make cheap there.
Summary
Functions
Runs each request, all of them at once, and collects the answers.
Types
Functions
Runs each request, all of them at once, and collects the answers.
A module that is unavailable — no process under its name, or one that is shutting down — is reported as timed out rather than taking the round with it, and a request that runs past the deadline is killed the same way. A request that raises is not swallowed: the round exits with the request's reason, so a bug in a module is a bug and not a module that answered nothing.