MLServe.ModelRegistry (MLServe v0.1.3)

Copy Markdown View Source

The model catalog: which models exist, at which versions, and where traffic goes.

Why this is a GenServer that is never called on the hot path

MLServe.predict/3 runs in whatever process is handling the request — a Phoenix controller, an Oban worker, a Task. If every prediction had to GenServer.call this process just to find out which worker to talk to, this single process would serialise the entire node's inference traffic before any inference happened.

So the registry owns a :protected ETS table with read_concurrency: true, and:

  • Readsroute/2, status/2, list/0 — run :ets.lookup/2 directly in the calling process. Lock-free, and they scale with schedulers.
  • Writes — registering, promoting, unregistering — go through the GenServer, which is the only process with write access. Serialising writes is free because they happen at load and deploy time, not per request.

Table layout

KeyValuePurpose
{:route, name, version}MLServe.RouteHot path. Slim by design — see MLServe.Route.
{:model, name, version}mapFull detail for MLServe.model_status/2.
{:default, name}versionWhich version unpinned traffic gets.
{:canary, name}{version, weight}Progressive rollout.

Splitting the hot-path route from the full entry keeps the per-prediction ETS copy to a few dozen words instead of copying the backend's entire configuration.

Summary

Functions

Returns the active canary as {version, weight}, or nil.

Returns a specification to start this module under a supervisor.

Returns the version unpinned traffic is routed to.

Fetches the route for an exact {name, version} pair, ready or not.

Returns every registered model name, sorted and deduplicated.

Fetches a route regardless of status. Used by lifecycle code, not by dispatch.

Resolves a model name to the route that should serve this request.

Returns the full status entry for a model version.

Returns every registered version of name, newest registration last.

Functions

canary(name)

@spec canary(atom()) :: {String.t(), 1..100} | nil

Returns the active canary as {version, weight}, or nil.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

default_version(name)

@spec default_version(atom()) :: {:ok, String.t()} | {:error, :model_not_found}

Returns the version unpinned traffic is routed to.

fetch_route(name, version)

@spec fetch_route(atom(), String.t()) ::
  {:ok, MLServe.Route.t()} | {:error, :model_not_found | :model_not_ready}

Fetches the route for an exact {name, version} pair, ready or not.

list()

@spec list() :: [atom()]

Returns every registered model name, sorted and deduplicated.

peek_route(name, version)

@spec peek_route(atom(), String.t()) ::
  {:ok, MLServe.Route.t()} | {:error, :model_not_found}

Fetches a route regardless of status. Used by lifecycle code, not by dispatch.

route(name, opts \\ [])

@spec route(
  atom(),
  keyword()
) :: {:ok, MLServe.Route.t()} | {:error, :model_not_found | :model_not_ready}

Resolves a model name to the route that should serve this request.

Runs entirely in the calling process. Version selection, in order:

  1. An explicit version: "2.1.0" option pins that version exactly.
  2. Otherwise, if a canary is configured, a per-request roll sends weight% of traffic to the candidate. :rand.uniform/1 uses the caller's own seed — no shared state, no coordination.
  3. Otherwise the default version.

Returns {:error, :model_not_ready} rather than :model_not_found when the model exists but is loading, draining or failed, because those are different operational problems.

status(name, version)

@spec status(atom(), String.t()) :: {:ok, map()} | {:error, :model_not_found}

Returns the full status entry for a model version.

versions(name)

@spec versions(atom()) :: {:ok, [String.t()]} | {:error, :model_not_found}

Returns every registered version of name, newest registration last.