85 lines
2.2 KiB
Elixir
85 lines
2.2 KiB
Elixir
defmodule Mv.Membership.SettingsCache do
|
||
@moduledoc """
|
||
Process-based cache for global settings to avoid repeated DB reads on hot paths
|
||
(e.g. RegistrationEnabled validation, Layouts.public_page, Plugs).
|
||
|
||
Uses a short TTL (default 60 seconds). Cache is invalidated on every settings
|
||
update so that changes take effect quickly. If no settings process exists
|
||
(e.g. in tests), get/1 falls back to direct read.
|
||
"""
|
||
use GenServer
|
||
|
||
@default_ttl_seconds 60
|
||
|
||
def start_link(opts \\ []) do
|
||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||
end
|
||
|
||
@doc """
|
||
Returns cached settings or fetches and caches them. Uses TTL; invalidate on update.
|
||
"""
|
||
def get do
|
||
case Process.whereis(__MODULE__) do
|
||
nil ->
|
||
# No cache process (e.g. test) – read directly
|
||
do_fetch()
|
||
|
||
_pid ->
|
||
GenServer.call(__MODULE__, :get, 10_000)
|
||
end
|
||
end
|
||
|
||
@doc """
|
||
Invalidates the cache so the next get/0 will refetch from the database.
|
||
Call after update_settings and any other path that mutates settings.
|
||
"""
|
||
def invalidate do
|
||
case Process.whereis(__MODULE__) do
|
||
nil -> :ok
|
||
_pid -> GenServer.cast(__MODULE__, :invalidate)
|
||
end
|
||
end
|
||
|
||
@impl true
|
||
def init(opts) do
|
||
ttl = Keyword.get(opts, :ttl_seconds, @default_ttl_seconds)
|
||
state = %{ttl_seconds: ttl, cached: nil, expires_at: nil}
|
||
{:ok, state}
|
||
end
|
||
|
||
@impl true
|
||
def handle_call(:get, _from, state) do
|
||
now = System.monotonic_time(:second)
|
||
expired? = state.expires_at == nil or state.expires_at <= now
|
||
|
||
{result, new_state} =
|
||
if expired? do
|
||
fetch_and_cache(now, state)
|
||
else
|
||
{{:ok, state.cached}, state}
|
||
end
|
||
|
||
{:reply, result, new_state}
|
||
end
|
||
|
||
defp fetch_and_cache(now, state) do
|
||
case do_fetch() do
|
||
{:ok, settings} = ok ->
|
||
expires = now + state.ttl_seconds
|
||
{ok, %{state | cached: settings, expires_at: expires}}
|
||
|
||
err ->
|
||
result = if state.cached, do: {:ok, state.cached}, else: err
|
||
{result, state}
|
||
end
|
||
end
|
||
|
||
@impl true
|
||
def handle_cast(:invalidate, state) do
|
||
{:noreply, %{state | cached: nil, expires_at: nil}}
|
||
end
|
||
|
||
defp do_fetch do
|
||
Mv.Membership.get_settings_uncached()
|
||
end
|
||
end
|