Public facade for Fly.io API token resolution.
Per-app tokens are resolved in this order (first hit wins):
- Shared ETS cache (fast path, lock-free from the caller's process).
- 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.yml→fly tokens create readonlyCLI → 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:facadeto:app_serveris 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.
Atomic invalidate/1 + get/1.
Store a manual override token for app_name (used as a last-resort
fallback in the resolution chain).
Functions
Returns a token for app_name, acquiring it from the resolution chain if
needed.
@spec invalidate(String.t()) :: :ok
Invalidate the ETS cache entry for app_name, forcing re-acquisition.
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.
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.