ExSandbox.Egress.Allocator (ExSandbox v1.0.1)

Copy Markdown View Source

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.

Summary

Types

Why an acquisition was refused.

Functions

Takes the next free /30, or refuses.

Returns a specification to start this module under a supervisor.

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

Types

refusal()

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

Why an acquisition was refused.

Functions

acquire(server \\ ExSandbox.Egress.Allocator)

@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(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

release(key, policy_gone?, server \\ ExSandbox.Egress.Allocator)

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.