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