# `Mutare.Runner.Partitions`
[🔗](https://github.com/foxbenjaminfox/mutare/blob/v0.5.1/lib/mutare/runner/partitions.ex#L1)

Per-worker partition slots, for DB (or any resource) isolation across the
concurrent mutant runs.

When `:partition_env` names an environment variable, every concurrently-running
mutant `mix test` process is given a **distinct** partition id under that name,
drawn from a bounded, recycled pool. The user's `config/test.exs` reads it to
pick a per-worker database:

    config :my_app, MyApp.Repo,
      database: "my_app_test#{System.get_env("MIX_TEST_PARTITION")}"

— exactly the `mix test --partitions` convention (default name
`MIX_TEST_PARTITION`), so two live runs never collide on one database.

## Why a pool, not `rem(index, workers)`

`Task.async_stream` hands each item no stable lane index, and `rem(index,
workers)` is **unsafe**: tasks don't finish in index order, so task `0` (→
partition 1) can still be running when task `workers` (→ partition 1) starts —
two live runs on one database. So this is a **checkout/checkin pool** of
`workers` tokens (partitions `1..workers`): a task checks out a free partition
before spawning `mix`, runs it (harness retries included, since they recurse
inside the same task), then checks it back in. A counting argument makes
checkout non-blocking: when a task checks out it is itself alive, so the other
alive tasks (≤ `workers - 1`) hold ≤ `workers - 1` tokens, leaving ≥ 1 free.

The baseline and coverage probe run sequentially *before* the pool, so they take
a fixed partition (`entry/2` with `1`) rather than a pooled slot — there is no
concurrency to isolate there, and a partitioned suite still needs *some* valid
partition to find its database.

## Disabled

A `nil` env name disables everything: `new/2` returns `:disabled`, `with_slot/2`
yields `nil` (no slot), and `entry/2` of the `nil` name and `slot_entry/2` of
the `nil` slot are `[]` — so the partition feature is pure opt-in and inert by
default. The pool is a small `Agent` holding the free tokens; `Mutare.Runner`
manages its lifecycle (`new/2` … `stop/1`).

# `t`

```elixir
@opaque t()
```

# `entry`

```elixir
@spec entry(String.t() | nil, pos_integer()) :: [{String.t(), String.t()}]
```

Env entries for a *fixed* partition `n` (the baseline and coverage probe use
`1`): `[]` when partitioning is off (`env_name` is `nil`), else `[{env_name,
"#{n}"}]`. Pure, so the env contract is unit-testable.

    iex> Mutare.Runner.Partitions.entry(nil, 1)
    []

    iex> Mutare.Runner.Partitions.entry("MIX_TEST_PARTITION", 3)
    [{"MIX_TEST_PARTITION", "3"}]

# `max_concurrency`

```elixir
@spec max_concurrency(t(), pos_integer()) :: pos_integer()
```

The `Task.async_stream` `max_concurrency` that keeps the non-blocking checkout
invariant: the pool size when partitioning is on (one token per lane), else
`default` (no pool, so nothing to bound — use the caller's worker count). Drive
`max_concurrency` from this so it can never drift from the token count.

    iex> Mutare.Runner.Partitions.max_concurrency(:disabled, 8)
    8

# `new`

```elixir
@spec new(String.t() | nil, pos_integer()) :: t()
```

Build a slot pool of `size` tokens (partitions `1..size`) for the variable
`env_name`, or `:disabled` when `env_name` is `nil`. `size` is the worker count,
so there is exactly one token per concurrency lane.

# `slot_entry`

```elixir
@spec slot_entry(t(), pos_integer() | nil) :: [{String.t(), String.t()}]
```

Env entries for `slot`, a partition of `pool`'s (a number `with_slot/2` yielded, or one
a result recorded): `[]` for the disabled pool's `nil`, else `entry/2` under the pool's
variable. The pool, not the options, names the variable, so a slot can never be
paired with a variable from a different configuration; a slot outside the pool raises.

# `stop`

```elixir
@spec stop(t()) :: :ok
```

Stop the pool (a no-op when disabled).

# `with_slot`

```elixir
@spec with_slot(t(), (pos_integer() | nil -&gt; result)) :: result when result: var
```

Check out a free partition, call `fun` with its number, and check the partition
back in afterwards — even if `fun` raises. `slot_entry/2` turns the number into the
run's env entry, and a result records it, so a run's partition can be compared
with the others' afterwards. When disabled, calls `fun.(nil)`.

---

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