Skip to content

Instantly share code, notes, and snippets.

@rajrajhans
Created March 4, 2023 10:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rajrajhans/01f73dc44f0be3682de807abe95430b2 to your computer and use it in GitHub Desktop.
Save rajrajhans/01f73dc44f0be3682de807abe95430b2 to your computer and use it in GitHub Desktop.
Refreshing Third Party Tokens before they expire in Elixir using GenServers
# https://rajrajhans.com/2023/02/refreshing-tokens-before-expiry-in-elixir/
defmodule MyApplication.TokenCache do
use GenServer
alias MyApplication.RandomApi
@refresh_lead_time_ms :timer.minutes(5)
@token_key :random_api
require Logger
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, :ok, Keyword.put(opts, :name, __MODULE__))
end
def get() do
:ets.lookup(__MODULE__, @token_key)
|> refresh_auth_if_required()
|> parse_cached_auth
end
## GenServer Callbacks
def init(:ok) do
ets = :ets.new(__MODULE__, [:named_table, read_concurrency: true])
{:ok, ets}
end
def handle_call(:refresh_auth, _from, ets) do
auth = refresh_auth(ets)
{:reply, auth, ets}
end
def handle_info(:refresh_auth, ets) do
refresh_auth(ets)
{:noreply, ets}
end
### Token Cache Helpers
defp refresh_auth_if_required([]) do
GenServer.call(__MODULE__, :refresh_auth, 30_000)
end
defp refresh_auth_if_required([{_key, cached_auth}]) do
if should_refresh_auth?(cached_auth) do
Logger.info("Auth token is about to expire, refreshing it now")
GenServer.call(__MODULE__, :refresh_auth, 30_000)
else
cached_auth
end
end
defp refresh_auth(ets) do
:ets.lookup(__MODULE__, @token_key)
|> refresh_auth_if_stale(ets)
end
defp refresh_auth_if_stale([], ets) do
refresh_auth_now(ets)
end
defp refresh_auth_if_stale([{_key, cached_auth}], ets) do
if should_refresh_auth?(cached_auth) do
Logger.info("Auth token is about to expire, refreshing it now")
refresh_auth_now(ets)
else
# we still have a valid auth token, so simply return that
cached_auth
end
end
defp refresh_auth_if_stale(_, ets) do
refresh_auth_now(ets)
end
defp refresh_auth_now(ets) do
with {:ok, auth} <- get_auth() do
:ets.insert(ets, {@token_key, auth})
Process.send_after(__MODULE__, :refresh_auth, next_refresh_in(auth))
auth
else
{:error, _e} ->
# schedule a retry in 5 seconds on error
Process.send_after(__MODULE__, :refresh_auth, 5_000)
end
end
defp next_refresh_in(%{expiration_s: expiration_s, token_issued_at: token_issued_at}) do
expires_in_ms = expiration_s * 1000
token_age_ms = get_token_age_s(token_issued_at) * 1000
next_refresh_in_ms = expires_in_ms - token_age_ms
max(0, next_refresh_in_ms - @refresh_lead_time_ms)
end
defp next_refresh_in(_), do: raise("Invalid auth struct")
defp get_auth() do
with {:ok, token, expiration} <- RandomApi.get_access_token() do
{:ok,
%{
expiration_s: expiration,
token: token,
token_issued_at: DateTime.utc_now()
}}
else
{:error, e} ->
Logger.error("Error getting the auth token: #{inspect(e)}")
{:error, e}
end
end
defp parse_cached_auth(%{token: token}) do
{:ok, token}
end
defp parse_cached_auth(_), do: {:error, :invalid_auth}
defp should_refresh_auth?(auth) do
next_refresh_in(auth) <= 0 || is_auth_expired?(auth)
end
defp is_auth_expired?(%{
expiration_s: expiration_s,
token_issued_at: token_issued_at,
token: _token
}) do
token_age_s = get_token_age_s(token_issued_at)
token_age_s >= expiration_s
end
defp get_token_age_s(token_issued_at) do
DateTime.diff(DateTime.utc_now(), token_issued_at, :second)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment