GhEx.TokenCache.ETS (gh_ex v0.3.2)

Copy Markdown View Source

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

child_spec(init_arg)

Builds a supervisor child spec whose :id is the :name, so several named caches can run under one supervisor without an id clash.

start_link(opts \\ [])

Starts the cache. :name defaults to GhEx.TokenCache.ETS.