Packages
fnord
0.9.38
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/services/globals.ex
defmodule Services.Globals do
@moduledoc """
Drop-in-ish replacement for Application env that *shadows* values down a
process tree. Think: dynamic scope via process ancestry.
- `put_env/3` sets an override in the *current tree* (installing the caller as a root if needed).
- `get_env/3` first checks the current tree's overrides, then falls back to `Application.get_env/3`.
- `delete_env/2` removes the tree-local override.
- `get_all_env/1` lists all overrides in the current tree, overlaying them on top of `Application.get_all_env/1` if the caller is the root.
- `put_all_env/2` bulk-inserts multiple overrides for one or more apps in the current tree (installing the caller as a root if needed).
- `install_root/0` explicitly installs the caller as a shadowing root (rarely needed; `put_env/3` auto-installs).
- `current_root/0` returns the current shadowing root PID (or nil). Useful for debugging.
- `explain/0` prints the current process tree and its overrides (for debugging).
"""
use GenServer
@pd_root_key :globals_root_pid
@roots_tab :globals_roots
@data_tab :globals_data
@type app :: atom()
@type key :: term()
@type value :: term()
# ----------------------------------------------------------------------------
# Public API
# ----------------------------------------------------------------------------
def start_link(opts \\ []) do
opts = Keyword.put_new(opts, :name, __MODULE__)
case GenServer.start_link(__MODULE__, :ok, opts) do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
end
end
@doc """
Put a tree-local override. Creates a root for the current process if none
exists.
"""
@spec put_env(atom, term, term) :: :ok
def put_env(app, key, value) do
root = ensure_root!()
true = :ets.insert(@data_tab, {{root, app, key}, value})
:ok
end
@doc """
Get a value with tree-local shadowing, else falls back to
`Application.get_env/3`.
"""
@spec get_env(atom, term, term) :: term
def get_env(app, key, default \\ nil) do
root = resolve_root()
found =
case root do
nil ->
Application.get_env(app, key, default)
root when root == self() ->
# We *are* the root: check overrides, else fall back directly
case :ets.lookup(@data_tab, {root, app, key}) do
[{{^root, ^app, ^key}, value}] -> value
[] -> Application.get_env(app, key, default)
end
root ->
# We're under a root: check overrides, else just return default
case :ets.lookup(@data_tab, {root, app, key}) do
[{{^root, ^app, ^key}, value}] -> value
[] -> default
end
end
found
end
@doc """
Get a tree-local override only, with no fallback to `Application.get_env/3`.
Returns `default` when the caller is not under a root or no override is set.
Use this for keys that have no meaningful Application-env counterpart (e.g.
`Services.Instance` registrations, whose keys are not atoms and whose values
are pids scoped to a single tree's lifetime).
"""
@spec get_override(atom, term, term) :: term
def get_override(app, key, default \\ nil) do
case resolve_root() do
nil ->
default
root ->
case :ets.lookup(@data_tab, {root, app, key}) do
[{{^root, ^app, ^key}, value}] -> value
[] -> default
end
end
end
@doc """
Delete a tree-local override (no-op if none). Returns :ok.
"""
@spec delete_env(atom, term) :: :ok
def delete_env(app, key) do
case resolve_root() do
nil ->
:ok
root ->
:ets.delete(@data_tab, {root, app, key})
:ok
end
end
@doc """
Bulk put multiple overrides for the given app in the current tree
(installing the caller as a root if needed).
"""
@spec put_all_env(app(), [{key(), value()}]) :: :ok
def put_all_env(app, kvs) when is_atom(app) and is_list(kvs) do
root = ensure_root!()
Enum.each(kvs, fn {k, v} ->
true = :ets.insert(@data_tab, {{root, app, k}, v})
end)
:ok
end
@doc """
Get all tree-local overrides for the given app, overlaying them on top of
`Application.get_all_env/1` if the caller is the root.
Only atom-keyed overrides are included, mirroring Application env
semantics. Non-atom keys (e.g. `Services.Instance` registrations) are
visible via `overrides/1` instead - including them here would crash
`Keyword.put/3`.
"""
@spec get_all_env(atom()) :: keyword()
def get_all_env(app) do
case resolve_root() do
nil ->
Application.get_all_env(app)
root when root == self() ->
# We *are* the root: overlay overrides on top of Application env
# (overrides win).
base = Application.get_all_env(app)
root
|> overrides_for(app)
|> Enum.filter(fn {k, _v} -> is_atom(k) end)
|> Enum.reduce(base, fn {k, v}, acc ->
Keyword.put(acc, k, v)
end)
root ->
# Descendant: show only overrides (matches your get_env/3 semantics)
overrides_for(root, app)
|> Enum.filter(fn {k, _v} -> is_atom(k) end)
|> Enum.sort_by(&elem(&1, 0))
end
end
@doc """
All tree-local overrides for the given app as raw `{key, value}` tuples,
with no Application-env overlay and no atom-key restriction. This is the
introspection surface for non-atom keys such as `Services.Instance`
registrations.
"""
@spec overrides(atom()) :: [{term(), term()}]
def overrides(app) do
case resolve_root() do
nil -> []
root -> overrides_for(root, app)
end
end
@doc """
Idempotently create a named, public ETS table owned by the Globals server.
Use this instead of check-then-:ets.new in caller code: the bare pattern
races under concurrency (both callers see :undefined, the loser crashes),
and a table created by an arbitrary process dies with that process -
under per-test instances, that means whichever test got there first.
Creation is serialized through the Globals GenServer, and Globals lives
for the whole VM, so the table is race-free and permanent.
"""
@spec ensure_shared_table(atom(), list()) :: :ok
def ensure_shared_table(name, opts) when is_atom(name) and is_list(opts) do
GenServer.call(__MODULE__, {:ensure_shared_table, name, opts})
end
@doc """
Install the caller as a shadowing root explicitly (rarely needed; put_env/3
auto-installs).
"""
@spec install_root() :: :ok
def install_root() do
GenServer.call(__MODULE__, {:install_root, self()})
end
@doc """
Return the current shadowing root PID (or nil). Useful for debugging.
"""
@spec current_root() :: pid | nil
def current_root(), do: resolve_root()
def explain() do
root = resolve_root()
IO.puts("Globals process tree (root = #{inspect(root)})")
do_explain(root, 0)
:ok
end
defp do_explain(pid, depth) when is_pid(pid) do
indent = String.duplicate(" ", depth)
# Which apps/keys are set for this pid?
entries =
:ets.select(@data_tab, [
{{{pid, :"$1", :"$2"}, :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}
])
if entries != [] do
IO.puts("#{indent}- #{inspect(pid)}")
Enum.each(entries, fn {app, key, val} ->
IO.puts("#{indent} #{inspect(app)}.#{inspect(key)} = #{inspect(val)}")
end)
else
IO.puts("#{indent}- #{inspect(pid)} (no overrides)")
end
# Recurse into linked children
case Process.info(pid, :links) do
{:links, links} ->
Enum.each(links, fn child ->
if is_pid(child) and Process.alive?(child) do
do_explain(child, depth + 1)
end
end)
_ ->
:ok
end
end
# ----------------------------------------------------------------------------
# GenServer callbacks
# ----------------------------------------------------------------------------
@impl true
def init(:ok) do
:ets.new(@roots_tab, [:named_table, :set, :public])
:ets.new(@data_tab, [:named_table, :set, :public, :compressed, read_concurrency: true])
{:ok, %{refs: %{}}}
end
@impl true
def handle_call({:ensure_shared_table, name, opts}, _from, s) do
case :ets.info(name) do
:undefined -> :ets.new(name, opts)
_info -> :ok
end
{:reply, :ok, s}
end
@impl true
def handle_call({:install_root, pid}, _from, s) do
if :ets.member(@roots_tab, pid) do
{:reply, :ok, s}
else
ref = Process.monitor(pid)
:ets.insert(@roots_tab, {pid, true})
# cache root in caller PD so descendants resolve quickly
if self() == pid do
Process.put(@pd_root_key, pid)
end
s = put_in(s.refs[ref], pid)
{:reply, :ok, s}
end
end
@impl true
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{refs: refs} = s) do
case Map.pop(refs, ref) do
{nil, _} ->
{:noreply, s}
{root, refs2} ->
# Remove root marker
:ets.delete(@roots_tab, root)
# Wipe all overrides under this root
# Match head: {{root, _app, _key}, _val}
ms = [{{{root, :_, :_}, :_}, [], [true]}]
:ets.select_delete(@data_tab, ms)
{:noreply, %{s | refs: refs2}}
end
end
# ----------------------------------------------------------------------------
# Internal
# ----------------------------------------------------------------------------
# Ensure there is a root for this process tree; if none, install self().
defp ensure_root!() do
case resolve_root() do
nil ->
:ok = install_root()
self()
pid ->
pid
end
end
# ----------------------------------------------------------------------------
# Resolve the current root PID:
# 1) PD cache
# 2) Self as root (fast path)
# 3) Walk :"$ancestors" for first PID in @roots_tab
#
# Before the Globals server has started (e.g. env reads during escript
# startup, ahead of Fnord.Instance), the tables don't exist yet. That is
# simply "no root installed anywhere" - return nil so get_env falls back to
# Application.get_env rather than crashing on the missing table.
# ----------------------------------------------------------------------------
defp resolve_root() do
if :ets.whereis(@roots_tab) == :undefined do
nil
else
do_resolve_root()
end
end
defp do_resolve_root() do
case Process.get(@pd_root_key) do
pid when is_pid(pid) ->
if :ets.member(@roots_tab, pid) do
pid
else
nil
end
_other ->
cond do
:ets.member(@roots_tab, self()) ->
Process.put(@pd_root_key, self())
self()
true ->
case Process.get(:"$ancestors") do
list when is_list(list) ->
case Enum.find(list, &:ets.member(@roots_tab, &1)) do
nil ->
nil
root ->
Process.put(@pd_root_key, root)
root
end
_ ->
nil
end
end
end
end
@spec overrides_for(pid(), atom()) :: keyword()
defp overrides_for(root, app) do
# Robust and simple: match → map to {k,v}
@data_tab
|> :ets.match({{root, app, :"$1"}, :"$2"})
|> Enum.map(fn [k, v] -> {k, v} end)
end
end