1 |
|
-module(gen_hook). |
2 |
|
|
3 |
|
-behaviour(gen_server). |
4 |
|
|
5 |
|
%% External exports |
6 |
|
-export([start_link/0, |
7 |
|
add_handler/5, |
8 |
|
delete_handler/5, |
9 |
|
add_handlers/1, |
10 |
|
delete_handlers/1, |
11 |
|
run_fold/4]). |
12 |
|
-export([reload_hooks/0]). |
13 |
|
|
14 |
|
%% gen_server callbacks |
15 |
|
-export([init/1, |
16 |
|
handle_call/3, |
17 |
|
handle_cast/2, |
18 |
|
code_change/3, |
19 |
|
handle_info/2, |
20 |
|
terminate/2]). |
21 |
|
|
22 |
|
%% exported for unit tests only |
23 |
|
-export([error_running_hook/5]). |
24 |
|
|
25 |
|
-ignore_xref([start_link/0, add_handlers/1, delete_handlers/1]). |
26 |
|
|
27 |
|
-include("safely.hrl"). |
28 |
|
-include("mongoose.hrl"). |
29 |
|
|
30 |
|
-type hook_name() :: atom(). |
31 |
|
-type hook_tag() :: mongooseim:host_type_or_global(). |
32 |
|
|
33 |
|
%% while Accumulator is not limited to any type, it's recommended to use maps. |
34 |
|
-type hook_acc() :: any(). |
35 |
|
-type hook_params() :: map(). |
36 |
|
-type hook_extra() :: map(). |
37 |
|
-type extra() :: #{hook_name := hook_name(), |
38 |
|
hook_tag := hook_tag(), |
39 |
|
host_type => mongooseim:host_type(), |
40 |
|
_ => _}. |
41 |
|
|
42 |
|
-type hook_fn_ret() :: hook_fn_ret(hook_acc()). |
43 |
|
-type hook_fn_ret(Acc) :: {ok | stop, Acc}. |
44 |
|
-type hook_fn() :: %% see run_fold/4 documentation |
45 |
|
fun((Accumulator :: hook_acc(), |
46 |
|
ExecutionParameters :: hook_params(), |
47 |
|
ExtraParameters :: extra()) -> hook_fn_ret()). |
48 |
|
|
49 |
|
-type key() :: {HookName :: atom(), |
50 |
|
Tag :: any()}. |
51 |
|
|
52 |
|
-type hook_tuple() :: hook_tuple(hook_fn()). |
53 |
|
-type hook_tuple(HookFn) :: {HookName :: hook_name(), |
54 |
|
Tag :: hook_tag(), |
55 |
|
Function :: HookFn, |
56 |
|
Extra :: hook_extra(), |
57 |
|
Priority :: pos_integer()}. |
58 |
|
|
59 |
|
-type hook_list() :: hook_list(hook_fn()). |
60 |
|
-type hook_list(HookFn) :: [hook_tuple(HookFn)]. |
61 |
|
|
62 |
|
-export_type([hook_name/0, |
63 |
|
hook_tag/0, |
64 |
|
hook_fn/0, |
65 |
|
hook_list/0, |
66 |
|
hook_list/1, |
67 |
|
hook_fn_ret/0, |
68 |
|
hook_fn_ret/1, |
69 |
|
hook_tuple/0, |
70 |
|
extra/0]). |
71 |
|
|
72 |
|
-record(hook_handler, {prio :: pos_integer(), |
73 |
|
hook_fn :: hook_fn(), |
74 |
|
extra :: extra()}). |
75 |
|
|
76 |
|
%%%---------------------------------------------------------------------- |
77 |
|
%%% API |
78 |
|
%%%---------------------------------------------------------------------- |
79 |
|
start_link() -> |
80 |
101 |
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). |
81 |
|
|
82 |
|
%% @doc Add a handler for a hook. |
83 |
|
%% Priority is used to sort the calls (lower numbers are executed first). |
84 |
|
-spec add_handler(HookName :: hook_name(), |
85 |
|
Tag :: hook_tag(), |
86 |
|
Function :: hook_fn(), |
87 |
|
Extra :: hook_extra(), |
88 |
|
Priority :: pos_integer()) -> ok. |
89 |
|
add_handler(HookName, Tag, Function, Extra, Priority) -> |
90 |
142 |
add_handler({HookName, Tag, Function, Extra, Priority}). |
91 |
|
|
92 |
|
-spec add_handlers(hook_list()) -> ok. |
93 |
|
add_handlers(List) -> |
94 |
6506 |
[add_handler(HookTuple) || HookTuple <- List], |
95 |
6506 |
ok. |
96 |
|
|
97 |
|
-spec add_handler(hook_tuple()) -> ok. |
98 |
|
add_handler({HookName, Tag, _, _, _} = HookTuple) -> |
99 |
44205 |
Handler = make_hook_handler(HookTuple), |
100 |
44205 |
Key = hook_key(HookName, Tag), |
101 |
44205 |
gen_server:call(?MODULE, {add_handler, Key, Handler}). |
102 |
|
|
103 |
|
%% @doc Delete a hook handler. |
104 |
|
%% It is important to indicate exactly the same information than when the call was added. |
105 |
|
-spec delete_handler(HookName :: hook_name(), |
106 |
|
Tag :: hook_tag(), |
107 |
|
Function :: hook_fn(), |
108 |
|
Extra :: hook_extra(), |
109 |
|
Priority :: pos_integer()) -> ok. |
110 |
|
delete_handler(HookName, Tag, Function, Extra, Priority) -> |
111 |
41 |
delete_handler({HookName, Tag, Function, Extra, Priority}). |
112 |
|
|
113 |
|
-spec delete_handlers(hook_list()) -> ok. |
114 |
|
delete_handlers(List) -> |
115 |
4520 |
[delete_handler(HookTuple) || HookTuple <- List], |
116 |
4520 |
ok. |
117 |
|
|
118 |
|
-spec delete_handler(hook_tuple()) -> ok. |
119 |
|
delete_handler({HookName, Tag, _, _, _} = HookTuple) -> |
120 |
29021 |
Handler = make_hook_handler(HookTuple), |
121 |
29021 |
Key = hook_key(HookName, Tag), |
122 |
29021 |
gen_server:call(?MODULE, {delete_handler, Key, Handler}). |
123 |
|
|
124 |
|
%% @doc Run hook handlers in order of priority (lower number means higher priority). |
125 |
|
%% * if a hook handler returns {ok, NewAcc}, the NewAcc value is used |
126 |
|
%% as an accumulator parameter for the following hook handler. |
127 |
|
%% * if a hook handler returns {stop, NewAcc}, execution stops immediately |
128 |
|
%% without invoking lower priority hook handlers. |
129 |
|
%% * if a hook handler crashes, the error is logged and the next hook handler |
130 |
|
%% is executed. |
131 |
|
%% Note that every hook handler MUST return a valid Acc. If a hook handler is not |
132 |
|
%% interested in changing Acc parameter (or even if Acc is not used for a hook |
133 |
|
%% at all), it must return (pass through) an unchanged input accumulator value. |
134 |
|
-spec run_fold(HookName :: hook_name(), |
135 |
|
Tag :: hook_tag(), |
136 |
|
Acc :: hook_acc(), |
137 |
|
Params :: hook_params()) -> hook_fn_ret(). |
138 |
|
run_fold(HookName, Tag, Acc, Params) -> |
139 |
224559 |
Key = hook_key(HookName, Tag), |
140 |
224559 |
case persistent_term:get(?MODULE, #{}) of |
141 |
|
#{Key := Ls} -> |
142 |
184676 |
mongoose_metrics:increment_generic_hook_metric(Tag, HookName), |
143 |
184676 |
run_hook(Ls, Acc, Params, Key); |
144 |
|
_ -> |
145 |
39883 |
{ok, Acc} |
146 |
|
end. |
147 |
|
|
148 |
|
reload_hooks() -> |
149 |
101 |
gen_server:call(?MODULE, reload_hooks). |
150 |
|
|
151 |
|
%%%---------------------------------------------------------------------- |
152 |
|
%%% gen_server callback functions |
153 |
|
%%%---------------------------------------------------------------------- |
154 |
|
|
155 |
|
init([]) -> |
156 |
101 |
erlang:process_flag(trap_exit, true), %% We need to make sure that terminate is called in tests |
157 |
101 |
{ok, #{}}. |
158 |
|
|
159 |
|
handle_call({add_handler, Key, #hook_handler{} = HookHandler}, _From, State) -> |
160 |
44205 |
NewState = |
161 |
|
case maps:get(Key, State, []) of |
162 |
|
[] -> |
163 |
29155 |
NewLs = [HookHandler], |
164 |
29155 |
create_hook_metric(Key), |
165 |
29155 |
maps:put(Key, NewLs, State); |
166 |
|
Ls -> |
167 |
15050 |
case lists:search(fun_is_handler_equal_to(HookHandler), Ls) of |
168 |
|
{value, _} -> |
169 |
116 |
?LOG_WARNING(#{what => duplicated_handler, |
170 |
:-( |
key => Key, handler => HookHandler}), |
171 |
116 |
State; |
172 |
|
false -> |
173 |
|
%% NOTE: sort *only* on the priority, |
174 |
|
%% order of other fields is not part of the contract |
175 |
14934 |
NewLs = lists:keymerge(#hook_handler.prio, Ls, [HookHandler]), |
176 |
14934 |
maps:put(Key, NewLs, State) |
177 |
|
end |
178 |
|
end, |
179 |
44205 |
maybe_insert_immediately(NewState), |
180 |
44205 |
{reply, ok, NewState}; |
181 |
|
handle_call({delete_handler, Key, #hook_handler{} = HookHandler}, _From, State) -> |
182 |
29021 |
NewState = |
183 |
|
case maps:get(Key, State, []) of |
184 |
|
[] -> |
185 |
13 |
State; |
186 |
|
Ls -> |
187 |
|
%% NOTE: The straightforward handlers comparison would compare |
188 |
|
%% the function objects, which is not well-defined in OTP. |
189 |
|
%% So we do a manual comparison on the MFA of the funs, |
190 |
|
%% by using `erlang:fun_info/2` |
191 |
29008 |
Pred = fun_is_handler_equal_to(HookHandler), |
192 |
29008 |
{_, NewLs} = lists:partition(Pred, Ls), |
193 |
29008 |
maps:put(Key, NewLs, State) |
194 |
|
end, |
195 |
29021 |
maybe_insert_immediately(NewState), |
196 |
29021 |
{reply, ok, NewState}; |
197 |
|
handle_call(reload_hooks, _From, State) -> |
198 |
101 |
persistent_term:put(?MODULE, State), |
199 |
101 |
{reply, ok, State}; |
200 |
|
handle_call(Request, From, State) -> |
201 |
:-( |
?UNEXPECTED_CALL(Request, From), |
202 |
:-( |
{reply, bad_request, State}. |
203 |
|
|
204 |
|
handle_cast(Msg, State) -> |
205 |
:-( |
?UNEXPECTED_CAST(Msg), |
206 |
:-( |
{noreply, State}. |
207 |
|
|
208 |
|
handle_info(Info, State) -> |
209 |
:-( |
?UNEXPECTED_INFO(Info), |
210 |
:-( |
{noreply, State}. |
211 |
|
|
212 |
|
terminate(_Reason, _State) -> |
213 |
101 |
persistent_term:erase(?MODULE), |
214 |
101 |
ok. |
215 |
|
|
216 |
|
code_change(_OldVsn, State, _Extra) -> |
217 |
:-( |
{ok, State}. |
218 |
|
|
219 |
|
%%%---------------------------------------------------------------------- |
220 |
|
%%% Internal functions |
221 |
|
%%%---------------------------------------------------------------------- |
222 |
|
|
223 |
|
%% @doc This call inserts the new hooks map immediately only if an existing map was already present. |
224 |
|
%% This simplifies tests: at startup we wait until all hooks have been accumulated |
225 |
|
%% before inserting them all at once, while during tests we don't need to remember |
226 |
|
%% to reload on every change |
227 |
|
maybe_insert_immediately(State) -> |
228 |
73226 |
case persistent_term:get(?MODULE, hooks_not_set) of |
229 |
|
hooks_not_set -> |
230 |
40560 |
ok; |
231 |
|
_ -> |
232 |
32666 |
persistent_term:put(?MODULE, State) |
233 |
|
end. |
234 |
|
|
235 |
|
-spec run_hook([#hook_handler{}], hook_acc(), hook_params(), key()) -> hook_fn_ret(). |
236 |
|
run_hook([], Acc, _Params, _Key) -> |
237 |
178486 |
{ok, Acc}; |
238 |
|
run_hook([Handler | Ls], Acc, Params, Key) -> |
239 |
235223 |
case apply_hook_function(Handler, Acc, Params) of |
240 |
|
{ok, NewAcc} -> |
241 |
229032 |
run_hook(Ls, NewAcc, Params, Key); |
242 |
|
{stop, NewAcc} -> |
243 |
6190 |
{stop, NewAcc}; |
244 |
|
{exception, Info} -> |
245 |
1 |
?MODULE:error_running_hook(Info, Handler, Acc, Params, Key), |
246 |
1 |
run_hook(Ls, Acc, Params, Key) |
247 |
|
end. |
248 |
|
|
249 |
|
-spec apply_hook_function(#hook_handler{}, hook_acc(), hook_params()) -> |
250 |
|
hook_fn_ret() | safely:exception(). |
251 |
|
apply_hook_function(#hook_handler{hook_fn = HookFn, extra = Extra}, |
252 |
|
Acc, Params) -> |
253 |
235223 |
?SAFELY(HookFn(Acc, Params, Extra)). |
254 |
|
|
255 |
|
error_running_hook(Info, Handler, Acc, Params, Key) -> |
256 |
1 |
?LOG_ERROR(Info#{what => hook_failed, |
257 |
|
text => <<"Error running hook">>, |
258 |
|
key => Key, |
259 |
|
handler => Handler, |
260 |
|
acc => Acc, |
261 |
:-( |
params => Params}). |
262 |
|
|
263 |
|
-spec make_hook_handler(hook_tuple()) -> #hook_handler{}. |
264 |
|
make_hook_handler({HookName, Tag, Function, Extra, Priority} = HookTuple) |
265 |
|
when is_atom(HookName), is_binary(Tag) or (Tag =:= global), |
266 |
|
is_function(Function, 3), is_map(Extra), |
267 |
|
is_integer(Priority), Priority > 0 -> |
268 |
73226 |
NewExtra = extend_extra(HookTuple), |
269 |
73226 |
check_hook_function(Function), |
270 |
73226 |
#hook_handler{prio = Priority, |
271 |
|
hook_fn = Function, |
272 |
|
extra = NewExtra}. |
273 |
|
|
274 |
|
-spec fun_is_handler_equal_to(#hook_handler{}) -> fun((#hook_handler{}) -> boolean()). |
275 |
|
fun_is_handler_equal_to(#hook_handler{prio = P0, hook_fn = HookFn0, extra = Extra0}) -> |
276 |
44058 |
Mod0 = erlang:fun_info(HookFn0, module), |
277 |
44058 |
Name0 = erlang:fun_info(HookFn0, name), |
278 |
44058 |
fun(#hook_handler{prio = P1, hook_fn = HookFn1, extra = Extra1}) -> |
279 |
71883 |
P0 =:= P1 andalso Extra0 =:= Extra1 andalso |
280 |
45559 |
Mod0 =:= erlang:fun_info(HookFn1, module) andalso |
281 |
28769 |
Name0 =:= erlang:fun_info(HookFn1, name) |
282 |
|
end. |
283 |
|
|
284 |
|
-spec check_hook_function(hook_fn()) -> ok. |
285 |
|
check_hook_function(Function) when is_function(Function, 3) -> |
286 |
73226 |
case erlang:fun_info(Function, type) of |
287 |
|
{type, external} -> |
288 |
73226 |
{module, Module} = erlang:fun_info(Function, module), |
289 |
73226 |
{name, FunctionName} = erlang:fun_info(Function, name), |
290 |
73226 |
case code:ensure_loaded(Module) of |
291 |
73226 |
{module, Module} -> ok; |
292 |
|
Error -> |
293 |
:-( |
throw_error(#{what => module_is_not_loaded, |
294 |
|
module => Module, error => Error}) |
295 |
|
end, |
296 |
73226 |
case erlang:function_exported(Module, FunctionName, 3) of |
297 |
73226 |
true -> ok; |
298 |
|
false -> |
299 |
:-( |
throw_error(#{what => function_is_not_exported, |
300 |
|
function => Function}) |
301 |
|
end; |
302 |
|
{type, local} -> |
303 |
:-( |
throw_error(#{what => only_external_function_references_allowed, |
304 |
|
function => Function}) |
305 |
|
end. |
306 |
|
|
307 |
|
-spec throw_error(map()) -> no_return(). |
308 |
|
throw_error(ErrorMap) -> |
309 |
:-( |
error(ErrorMap). |
310 |
|
|
311 |
|
-spec hook_key(HookName :: hook_name(), Tag :: hook_tag()) -> key(). |
312 |
|
hook_key(HookName, Tag) -> |
313 |
297785 |
{HookName, Tag}. |
314 |
|
|
315 |
|
-spec extend_extra(hook_tuple()) -> hook_extra(). |
316 |
|
extend_extra({HookName, Tag, _Function, OriginalExtra, _Priority}) -> |
317 |
73226 |
ExtraExtension = case Tag of |
318 |
978 |
global -> #{hook_name => HookName, hook_tag => Tag}; |
319 |
|
HostType when is_binary(HostType) -> |
320 |
72248 |
#{hook_name => HookName, hook_tag => Tag, |
321 |
|
host_type => HostType} |
322 |
|
end, |
323 |
|
%% KV pairs of the OriginalExtra map will remain unchanged, |
324 |
|
%% only the new keys from the ExtraExtension map will be added |
325 |
|
%% to the NewExtra map |
326 |
73226 |
maps:merge(ExtraExtension, OriginalExtra). |
327 |
|
|
328 |
|
-spec create_hook_metric(Key :: key()) -> any(). |
329 |
|
create_hook_metric({HookName, Tag}) -> |
330 |
29155 |
mongoose_metrics:create_generic_hook_metric(Tag, HookName). |