# `Mutare.Runner.Partitions`
[🔗](https://github.com/foxbenjaminfox/mutare/blob/v0.2.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 `[]` (no extra env), and `entry/2` is `[]` — 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.

# `stop`

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

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

# `with_slot`

```elixir
@spec with_slot(t(), ([{String.t(), String.t()}] -&gt; result)) :: result
when result: var
```

Check out a free partition, call `fun` with its env entries
(`[{env_name, "#{slot}"}]`), and check the partition back in afterwards — even
if `fun` raises. When disabled, calls `fun.([])` (no extra env).

---

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