-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
config.ex
166 lines (138 loc) · 4.41 KB
/
config.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
defmodule Phoenix.Config do
# Handles Phoenix configuration.
#
# This module is private to Phoenix and should not be accessed
# directly. The Phoenix endpoint configuration can be accessed
# at runtime using the `config/2` function.
@moduledoc false
require Logger
use GenServer
@doc """
Starts a Phoenix configuration handler.
"""
def start_link({module, config, defaults, opts}) do
permanent = Keyword.keys(defaults)
GenServer.start_link(__MODULE__, {module, config, permanent}, opts)
end
@doc """
Puts a given key-value pair in config.
"""
def put(module, key, value) do
:ets.insert(module, {key, value})
end
@doc """
Adds permanent configuration.
Permanent configuration is not deleted on hot code reload.
"""
def permanent(module, key, value) do
pid = :ets.lookup_element(module, :__config__, 2)
GenServer.call(pid, {:permanent, key, value})
end
@doc """
Caches a value in Phoenix configuration handler for the module.
The given function needs to return a tuple with `:cache` if the
value should be cached or `:nocache` if the value should not be
cached because it can be consequently considered stale.
Notice writes are not serialized to the server, we expect the
function that generates the cache to be idempotent.
"""
@spec cache(module, term, (module -> {:cache | :nocache, term})) :: term
def cache(module, key, fun) do
try do
:ets.lookup(module, key)
rescue
e ->
case :ets.info(module) do
:undefined ->
raise "could not find ets table for endpoint #{inspect(module)}. Make sure your endpoint is started and note you cannot access endpoint functions at compile-time"
_ ->
reraise e, __STACKTRACE__
end
else
[{^key, :cache, val}] ->
val
[] ->
case fun.(module) do
{:cache, val} ->
:ets.insert(module, {key, :cache, val})
val
{:nocache, val} ->
val
end
end
end
@doc """
Clears all cached entries in the endpoint.
"""
@spec clear_cache(module) :: :ok
def clear_cache(module) do
:ets.match_delete(module, {:_, :cache, :_})
:ok
end
@doc """
Reads the configuration for module from the given OTP app.
Useful to read a particular value at compilation time.
"""
def from_env(otp_app, module, defaults) do
config = fetch_config(otp_app, module)
merge(defaults, config)
end
@doc """
Take 2 keyword lists and merge them recursively.
Used to merge configuration values into defaults.
"""
def merge(a, b), do: Keyword.merge(a, b, &merger/3)
defp fetch_config(otp_app, module) do
case Application.fetch_env(otp_app, module) do
{:ok, conf} -> conf
:error -> []
end
end
defp merger(_k, v1, v2) do
if Keyword.keyword?(v1) and Keyword.keyword?(v2) do
Keyword.merge(v1, v2, &merger/3)
else
v2
end
end
@doc """
Changes the configuration for the given module.
It receives a keyword list with changed config and another
with removed ones. The changed config are updated while the
removed ones stop the configuration server, effectively removing
the table.
"""
def config_change(module, changed, removed) do
pid = :ets.lookup_element(module, :__config__, 2)
GenServer.call(pid, {:config_change, changed, removed})
end
# Callbacks
def init({module, config, permanent}) do
:ets.new(module, [:named_table, :public, read_concurrency: true])
update(module, config, [])
:ets.insert(module, {:__config__, self()})
{:ok, {module, [:__config__ | permanent]}}
end
def handle_call({:permanent, key, value}, _from, {module, permanent}) do
:ets.insert(module, {key, value})
{:reply, :ok, {module, [key | permanent]}}
end
def handle_call({:config_change, changed, removed}, _from, {module, permanent}) do
cond do
changed = changed[module] ->
update(module, changed, permanent)
{:reply, :ok, {module, permanent}}
module in removed ->
{:stop, :normal, :ok, {module, permanent}}
true ->
{:reply, :ok, {module, permanent}}
end
end
defp update(module, config, permanent) do
old_keys = :ets.select(module, [{{:"$1", :_}, [], [:"$1"]}])
new_keys = Enum.map(config, &elem(&1, 0))
Enum.each((old_keys -- new_keys) -- permanent, &:ets.delete(module, &1))
:ets.insert(module, config)
clear_cache(module)
end
end