# `Monty.Runtime.Barrier`
[🔗](https://github.com/d-led/ex.tbp.monty/blob/v0.1.0/packages/ex_tbp_monty_core/lib/monty/runtime/barrier.ex#L1)

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.

# `id`

```elixir
@type id() :: atom() | String.t()
```

# `request`

```elixir
@type request() :: {id(), (-&gt; term())}
```

# `result`

```elixir
@type result() :: %{replies: %{optional(id()) =&gt; term()}, timed_out: [id()]}
```

# `run`

```elixir
@spec run([request()], timeout()) :: result()
```

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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
