ConduitMcp.Session.EtsStore (ConduitMCP v0.11.0)

Copy Markdown View Source

Default in-memory session store backed by a public, named ETS table.

Sessions are keyed by session_id (a random URL-safe base64 string generated by ConduitMcp.Session.generate_id/0 on first initialize) and store an arbitrary metadata map plus "created_at" (millisecond Unix timestamp).

Ownership

The table is owned by a long-lived ConduitMcp.EtsOwner process (ConduitMcp.Session.EtsStore.Owner) started from ConduitMcp.Application, so it outlives the short-lived Bandit request process that happens to touch it first. A table owned by a request process would die with that request, taking every session with it — Session.get/2 would return {:error, :not_found} for a session id the client had just been handed.

ensure_table/0 remains as a fallback for embedding contexts where the :conduit_mcp application is not started (e.g. some test setups). It is not the primary mechanism.

Concurrency

The table is :public, :set, read_concurrency: true, write_concurrency: :auto. Reads do not block writes; writers serialize per key. Suitable for production single-node workloads. For multi-node deployments, implement a ConduitMcp.Session.Store against your distributed backend — see guides/multi_node_sessions.md.

Bounds & cleanup

Two mechanisms keep the table bounded, and both are needed:

  • Row cap. create/2 refuses to grow the table past config :conduit_mcp, :sessions_max_rows (default 100000, :infinity disables it) and returns {:error, :session_limit_reached}. Sessions are created by unauthenticated initialize requests, so without a cap a supervised (i.e. immortal) table is an unbounded growth surface.

    Note this is a global bound, not a per-caller one: it refuses new sessions rather than evicting existing ones, so an unauthenticated flood cannot displace a live session, but it can deny new ones until the janitor ages the flood out. ConduitMcp.Plugs.RateLimit is the per-caller bound; configure it on any public-facing transport.

  • TTL sweep. cleanup/1 prunes rows older than a TTL. ConduitMcp.Session.Janitor is started by ConduitMcp.Application against this store by default; disable it with config :conduit_mcp, :session_janitor, false, or tune it with config :conduit_mcp, :session_janitor, ttl: ..., interval: ....

Configuration

Reference the store from a transport's :session option:

{ConduitMcp.Transport.StreamableHTTP,
 server_module: MyServer,
 session: [store: ConduitMcp.Session.EtsStore, ttl: :timer.minutes(60)]}

Summary

Functions

Removes sessions older than ttl_ms milliseconds.

Creates a session, unless the table is already at its configured row cap.

Ensures the ETS table exists.

Functions

cleanup(ttl_ms)

Removes sessions older than ttl_ms milliseconds.

create(session_id, metadata)

Creates a session, unless the table is already at its configured row cap.

Returns :ok, or {:error, :session_limit_reached}.

ensure_table()

Ensures the ETS table exists.

Normally a no-op: ConduitMcp.Session.EtsStore.Owner creates the table at application start. Retained so the store still works when :conduit_mcp is not running as an OTP application.