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.
Summary
Functions
Returns a specification to start this module under a supervisor.
Starts a process named owner that creates and then owns table.
Types
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@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 to1000.