# `ExSandbox.Egress.Allocator`
[🔗](https://github.com/FoundryStack/ex_sandbox/blob/v1.2.0/lib/ex_sandbox/egress/allocator.ex#L1)

Hands out the `/30` a sandbox's netns is built on, and takes it back only
when the sandbox's policy is gone (005 T060a3, `contracts/egress.md`).

## Why this exists as its own module

`ExSandbox.Egress.Registry`'s moduledoc has always stated the invariant in
two halves — "`release/1` deletes the entry and only then returns the /30 to
the pool; `assign/2` refuses a /30 that still carries one". Only the second
half was enforced. There was no pool: `assign/2` takes whatever `source_key`
its caller supplies, and `:pool_exhausted` sat in the `refusal` type from the
first commit without any code path able to produce it.

⚠️ **A documented invariant with one half missing is worse than an undocumented
one**, because it reads as settled. Every reviewer since has seen "returns the
/30 to the pool" and had no reason to check whether a pool existed.

## The ordering, and why it is a callback rather than a convention

`release/3` takes a predicate that answers "is this /30's policy gone?" and
puts the address back **only if it answers true**. The alternative — document
that callers must release the policy first — is the ordering convention the
`Registry` moduledoc explicitly refuses to rely on, for the reason given
there: the correct ordering in a `destroy` callback is exactly what a later
refactor reorders without knowing why it was written that way.

Passing the check in means the allocator cannot be wrong about it. A caller
who releases the address while the policy stands does not corrupt the pool;
the /30 simply stays out until someone releases it again with the policy
actually gone.

## Why a free list rather than a counter

A counter that recycles on release makes the reuse race trivially reachable
and passes every "distinct sandboxes get distinct addresses" test — see
`ExSandbox.Egress.AllocatorTest`, which is written against exactly that
implementation.

# `refusal`

```elixir
@type refusal() ::
  :pool_exhausted | {:still_registered, ExSandbox.Egress.Policy.source_key()}
```

Why an acquisition was refused.

# `acquire`

```elixir
@spec acquire(GenServer.server()) ::
  {:ok, ExSandbox.Egress.Policy.source_key()} | {:error, refusal()}
```

Takes the next free `/30`, or refuses.

`{:error, {:still_registered, key}}` means the only remaining addresses are
ones whose policy has not been released — see the ordering note above.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `release`

```elixir
@spec release(
  ExSandbox.Egress.Policy.source_key(),
  (ExSandbox.Egress.Policy.source_key() -&gt; boolean()),
  GenServer.server()
) :: :ok
```

Returns `key` to the pool if `policy_gone?.(key)` says its policy is gone.

Idempotent, and safe for a `/30` this allocator never issued (`003-FR-013`):
destroy is reached twice for the same sandbox and must not fault the second
time.

---

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