# `ConduitMcp.EtsOwner`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/ets_owner.ex#L1)

Shared implementation for ConduitMCP's supervised ETS table owners.

Six subsystems keep state in `:public`, `:named_table` ETS tables that must
outlive the short-lived Bandit request process that happens to touch them
first: `ConduitMcp.Session.EtsStore`, `ConduitMcp.Tasks.EtsStore`,
`ConduitMcp.Ext.Tasks.EtsStore` (when the tasks extension is enabled),
`ConduitMcp.Cancellation` (its cancellation flags and its in-flight
requests, one table each), `ConduitMcp.Transport.SSE`'s slot table (one row
per live stream), and `ConduitMcp.OAuth.KeyProvider.JWKS`'s cache. Each
table has an owner process started by `ConduitMcp.Application` whose only
job is to create it and then idle, so the table's lifetime is the
application's.

The process holds no state beyond what it needs to re-claim, and answers no
calls - reads and writes go directly to the `:public` table from the calling
process. Do not turn any of them into a `handle_call` gateway.

## Why the create is not guarded

`init/1` calls `:ets.new/2` unconditionally. At boot the table does not exist
and owning it is the entire point; a `:ets.whereis` guard would let an
earlier `ensure_table/0` keep the table under a short-lived process while the
Owner stays alive owning nothing, silently defeating the guarantee.

## Why losing the race must not raise

`:ets.new/2` raises `ArgumentError` when the name is already taken. That can
happen legitimately: if an Owner exits, its table is destroyed, and any
process calling `ensure_table/0` before the supervisor's restart completes
becomes the new owner. The most likely racer is long-lived - a janitor tick
calls `cleanup/1`, which starts with `ensure_table/0` - so the Owner would
raise on every restart. Three restarts in five seconds take down
`ConduitMcp.Supervisor`, and with it the consumer's application, over a
cosmetic ownership question.

So a lost race logs and retries every second instead (`start_link/4`'s
`:reclaim_interval`, default 1000 ms). The retry matters: the racer is
usually a request or janitor process that exits within seconds, freeing the
name - a one-shot degrade would idle forever owning nothing while the table's
lifetime silently became one request's.

## Why only *that* ArgumentError is tolerated

`:ets.new/2` raises the same exception for invalid options, and the exception
carries no discriminator. Reporting a typo (`:naned_table`) as an ownership
race would send the operator chasing a race that never happened - in the one
module whose purpose is to make table ownership diagnosable. `init/1`
therefore re-raises unless the name really is taken.

# `table_opts`

```elixir
@type table_opts() :: [atom() | tuple()]
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `start_link`

```elixir
@spec start_link(module(), atom(), table_opts(), keyword()) :: GenServer.on_start()
```

Starts a process named `owner` that creates and then owns `table`.

## Options

  * `:reclaim_interval` — milliseconds between claim attempts after a lost
    race. Defaults to `1000`.

---

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