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 owns its lifecycle (new/2 … stop/1).
Summary
Functions
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.
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.
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 the pool (a no-op when disabled).
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).
Types
Functions
@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"}]
@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
@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.
@spec stop(t()) :: :ok
Stop the pool (a no-op when disabled).
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).