ExAtlas.Fly.Tokens (ExAtlas v0.5.0)

Copy Markdown View Source

Public facade for Fly.io API token resolution.

Per-app tokens are resolved in this order (first hit wins):

  1. Shared ETS cache (fast path, lock-free from the caller's process).
  2. Per-app ExAtlas.Fly.Tokens.AppServer — one process per app, started lazily on first miss. The AppServer runs the full resolution chain: durable storage → ~/.fly/config.ymlfly tokens create readonly CLI → manual override.

Concurrent callers for the same app coalesce at the AppServer's mailbox: the first caller does the CLI acquisition; subsequent callers wake up and re-check the ETS table (filled by the first caller) before descending the chain a second time. Concurrent callers for different apps run in parallel — each app has its own AppServer.

The supervision tree (ExAtlas.Fly.Tokens.Supervisor + ExAtlas.Fly.Tokens.ETSOwner + ExAtlas.Fly.Tokens.Registry + a DynamicSupervisor) is started by ExAtlas.Application when the Fly sub-tree is enabled (default).

Telemetry

Emits [:ex_atlas, :fly, :token, :acquire] span events (:start / :stop / :exception). :stop metadata includes:

  • app — the Fly app name.
  • source — which link in the chain produced the token (:ets / :storage / :config / :cli / :manual / :none).
  • acquirer — either :facade (pure ETS fast-path hit; no AppServer mailbox round-trip) or :app_server (slow-path resolution or coalesced cache hit on the AppServer side). The ratio of :facade to :app_server is a direct measure of how effective the cross-process ETS fast path is.

See guides/telemetry.md for the full reference.

Summary

Functions

Returns a token for app_name, acquiring it from the resolution chain if needed.

Invalidate the ETS cache entry for app_name, forcing re-acquisition.

Store a manual override token for app_name (used as a last-resort fallback in the resolution chain).

Functions

get(app_name)

@spec get(String.t()) :: {:ok, String.t()} | {:error, :no_token_available}

Returns a token for app_name, acquiring it from the resolution chain if needed.

invalidate(app_name)

@spec invalidate(String.t()) :: :ok

Invalidate the ETS cache entry for app_name, forcing re-acquisition.

refresh(app_name)

@spec refresh(String.t()) :: {:ok, String.t()} | {:error, :no_token_available}

Atomic invalidate/1 + get/1.

Equivalent to calling invalidate(app_name) followed by get(app_name), but executed under a single GenServer call on the AppServer so no concurrent caller can slip in between the two and observe the pre-refresh token.

Use this when you've learned a cached token is invalid (e.g. a 401 from the Fly API) and want to replace it atomically.

Returns the same shape as get/1.

set_manual(app_name, token)

@spec set_manual(String.t(), String.t()) ::
  :ok | {:error, {:persist_failed, String.t()}}

Store a manual override token for app_name (used as a last-resort fallback in the resolution chain).

Returns {:error, {:persist_failed, reason}} if the underlying storage raises — manual tokens are not re-acquirable, so the failure is surfaced rather than logged.