ConduitMcp.EtsOwner (ConduitMCP v0.11.0)

Copy Markdown View Source

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

table_opts()

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

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

start_link(owner, table, table_opts, opts \\ [])

@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.