This is my registry with some custom client code:
defmodule Auther.OAuth.Store.Registry do
use Supervisor
alias Auther.OAuth.Store.Entry
@registry_name __MODULE__
@dyn_supervisor_name __MODULE__.Supervisor
def start_link(_arg) do
Supervisor.start_link(__MODULE__, [])
end
@impl Supervisor
def init(_) do
children = [
{DynamicSupervisor, strategy: :one_for_one, name: @dyn_supervisor_name},
{Registry, keys: :unique, name: @registry_name}
]
Supervisor.init(children, strategy: :one_for_all)
end
@spec insert(key :: String.t(), value :: any) :: any
def insert(key, value) do
name = name_for_key(key)
DynamicSupervisor.start_child(@dyn_supervisor_name, {Entry, [name, value]})
end
defp name_for_key(key), do: {:via, Registry, {@registry_name, key}}
@spec fetch(key :: String.t()) :: {:ok, any} | {:error, :entry_not_found}
def fetch(key) do
res =
with [{pid, _val}] <- Registry.lookup(@registry_name, key),
true <- Process.alive?(pid), # todo this OK?
do: {:ok, Entry.get(pid)}
case res do
{:ok, _} = res -> res
_ -> {:error, :entry_not_found}
end
end
end
Here are the entries (GenServers):
defmodule Auther.OAuth.Store.Entry do
use GenServer, restart: :transient
require Logger
@expiry_time :timer.minutes(2)
@impl GenServer
def init(params) do
schedule_expiry()
{:ok, params}
end
defp schedule_expiry() do
Process.send_after(self(), :expired, @expiry_time)
end
@impl GenServer
def handle_info(:expired, state) do
Logger.debug("Temporary credentials expired.")
{:stop, :normal, state}
end
@impl GenServer
def handle_call(:get, _from, state) do
Logger.debug("Temporary credentials accessed, removing them now.")
{:stop, :normal, state, state}
end
#### CLIENT
def start_link([name, data]) do
GenServer.start_link(__MODULE__, data, name: name)
end
def get(pid) do
GenServer.call(pid, :get)
end
end
The goal is to have every entry either expire after 2min or remove itself when accessed once. This works by having the GenServer entries stop themselfes.
The problem is that in Registry.fetch/1
the Registry.lookup/2
function sometimes returns processes, that are already shut down or in the process of shutting down. I can consistently reproduce the problem if I call Registry.fetch/1
twice with the same key
argument. The first call returns the expected process and the message can be sent, the second call returns the same process but the message can’t be sent because it’s already stopped.
I have temporarily solved the problem by adding Process.alive?
into my with expression, but this seems fragile to me. Is there another approach I can use to make this more resilient?