Default GhEx.TokenCache: an ETS table owned by a GenServer.
Add it to your supervision tree so it outlives individual requests:
children = [
GhEx.TokenCache.ETS
# or, to run more than one or pick the name:
# {GhEx.TokenCache.ETS, name: MyApp.GitHubTokens}
]Then point installation clients at it:
inst = GhEx.App.installation(app, installation_id, cache: GhEx.TokenCache.ETS)Reads take the fast path straight from the public ETS table with no process hop. Misses and near-expiry refreshes go through the GenServer, which double-checks the table and then runs the mint in a monitored worker process, replying to every caller that joined while it ran. Concurrent callers for the same installation therefore trigger a single mint (single-flight), while mints for different installations run concurrently and never head-of-line-block each other. The GenServer itself never blocks on the network: a slow mint holds up only the callers waiting on that key, not the table or other keys.
Because the mint runs off the GenServer, fetch/3 waits on it with an
:infinity GenServer.call timeout: the mint's own deadline (the client's
Req receive_timeout, 15s by default, plus any opted-in
GhEx.RateLimit.retry/2 retry-after sleep) is the only deadline, exactly as
for a direct request through the same client. The previous default 5s
GenServer.call deadline could fire mid-mint and surface a raw
exit({:timeout, ...}) instead of {:ok, body, meta} | {:error, reason}.
A token is considered fresh until 60 seconds before its expires_at, so a
refresh happens slightly ahead of the real expiry.
Security: the table is :public
The table is created :named_table, :public, so any process in the same VM can
call :ets.lookup(GhEx.TokenCache.ETS, key) and read cached installation tokens
directly, bypassing this module's API. The :public access is load-bearing, not
incidental: reads run in the calling process for the lock-free fast path, which is
the whole point of the ETS cache. :protected would not change the trust boundary
(a protected table is still world-readable); only :private restricts reads, and
that would force every read back through the GenServer and give up the fast path.
Treat this as an inherent BEAM trust property rather than a privilege escalation: any same-VM process can already read another process's state and messages and any ETS table, so the cached token is no more exposed than the credentials that mint it. The trust boundary is the VM. If you need installation tokens isolated from other code, isolate at that level (a separate node or OS process), not inside one shared VM.
Summary
Functions
Builds a supervisor child spec whose :id is the :name, so several named
caches can run under one supervisor without an id clash.
Starts the cache. :name defaults to GhEx.TokenCache.ETS.
Functions
Builds a supervisor child spec whose :id is the :name, so several named
caches can run under one supervisor without an id clash.
Starts the cache. :name defaults to GhEx.TokenCache.ETS.