./ct_report/coverage/gen_hook.COVER.html

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 93 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 109 add_handler({HookName, Tag, Function, Extra, Priority}).
91
92 -spec add_handlers(hook_list()) -> ok.
93 add_handlers(List) ->
94 6623 [add_handler(HookTuple) || HookTuple <- List],
95 6623 ok.
96
97 -spec add_handler(hook_tuple()) -> ok.
98 add_handler({HookName, Tag, _, _, _} = HookTuple) ->
99 41516 Handler = make_hook_handler(HookTuple),
100 41516 Key = hook_key(HookName, Tag),
101 41516 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 16 delete_handler({HookName, Tag, Function, Extra, Priority}).
112
113 -spec delete_handlers(hook_list()) -> ok.
114 delete_handlers(List) ->
115 4793 [delete_handler(HookTuple) || HookTuple <- List],
116 4793 ok.
117
118 -spec delete_handler(hook_tuple()) -> ok.
119 delete_handler({HookName, Tag, _, _, _} = HookTuple) ->
120 29328 Handler = make_hook_handler(HookTuple),
121 29328 Key = hook_key(HookName, Tag),
122 29328 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 493060 Key = hook_key(HookName, Tag),
140 493060 case persistent_term:get(?MODULE, #{}) of
141 #{Key := Ls} ->
142 390857 mongoose_instrument_hooks:execute(HookName, Tag),
143 390856 run_hook(Ls, Acc, Params, Key);
144 _ ->
145 102203 {ok, Acc}
146 end.
147
148 reload_hooks() ->
149 93 gen_server:call(?MODULE, reload_hooks).
150
151 %%%----------------------------------------------------------------------
152 %%% gen_server callback functions
153 %%%----------------------------------------------------------------------
154
155 init([]) ->
156 93 erlang:process_flag(trap_exit, true), %% We need to make sure that terminate is called in tests
157 93 {ok, #{}}.
158
159 handle_call({add_handler, Key = {Name, Tag}, #hook_handler{} = HookHandler}, _From, State) ->
160 41516 NewState =
161 case maps:get(Key, State, []) of
162 [] ->
163 27109 NewLs = [HookHandler],
164 27109 mongoose_instrument_hooks:set_up(Name, Tag),
165 27109 maps:put(Key, NewLs, State);
166 Ls ->
167 14407 case lists:search(fun_is_handler_equal_to(HookHandler), Ls) of
168 {value, _} ->
169 162 ?LOG_WARNING(#{what => duplicated_handler,
170
:-(
key => Key, handler => HookHandler}),
171 162 State;
172 false ->
173 %% NOTE: sort *only* on the priority,
174 %% order of other fields is not part of the contract
175 14245 NewLs = lists:keymerge(#hook_handler.prio, Ls, [HookHandler]),
176 14245 maps:put(Key, NewLs, State)
177 end
178 end,
179 41516 maybe_insert_immediately(NewState),
180 41516 {reply, ok, NewState};
181 handle_call({delete_handler, Key = {Name, Tag}, #hook_handler{} = HookHandler}, _From, State) ->
182 29328 NewState =
183 case maps:get(Key, State, []) of
184 [] ->
185 128 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 29200 Pred = fun_is_handler_equal_to(HookHandler),
192 29200 {_, NewLs} = lists:partition(Pred, Ls),
193 29200 case NewLs of
194 16667 [] -> mongoose_instrument_hooks:tear_down(Name, Tag);
195 12533 _ -> ok
196 end,
197 29200 maps:put(Key, NewLs, State)
198 end,
199 29328 maybe_insert_immediately(NewState),
200 29328 {reply, ok, NewState};
201 handle_call(reload_hooks, _From, State) ->
202 93 persistent_term:put(?MODULE, State),
203 93 {reply, ok, State};
204 handle_call(Request, From, State) ->
205
:-(
?UNEXPECTED_CALL(Request, From),
206
:-(
{reply, bad_request, State}.
207
208 handle_cast(Msg, State) ->
209
:-(
?UNEXPECTED_CAST(Msg),
210
:-(
{noreply, State}.
211
212 handle_info(Info, State) ->
213
:-(
?UNEXPECTED_INFO(Info),
214
:-(
{noreply, State}.
215
216 terminate(_Reason, _State) ->
217 93 persistent_term:erase(?MODULE),
218 93 ok.
219
220 code_change(_OldVsn, State, _Extra) ->
221
:-(
{ok, State}.
222
223 %%%----------------------------------------------------------------------
224 %%% Internal functions
225 %%%----------------------------------------------------------------------
226
227 %% @doc This call inserts the new hooks map immediately only if an existing map was already present.
228 %% This simplifies tests: at startup we wait until all hooks have been accumulated
229 %% before inserting them all at once, while during tests we don't need to remember
230 %% to reload on every change
231 maybe_insert_immediately(State) ->
232 70844 case persistent_term:get(?MODULE, hooks_not_set) of
233 hooks_not_set ->
234 36104 ok;
235 _ ->
236 34740 persistent_term:put(?MODULE, State)
237 end.
238
239 -spec run_hook([#hook_handler{}], hook_acc(), hook_params(), key()) -> hook_fn_ret().
240 run_hook([], Acc, _Params, _Key) ->
241 371790 {ok, Acc};
242 run_hook([Handler | Ls], Acc, Params, Key) ->
243 533547 case apply_hook_function(Handler, Acc, Params) of
244 {ok, NewAcc} ->
245 514473 run_hook(Ls, NewAcc, Params, Key);
246 {stop, NewAcc} ->
247 19065 {stop, NewAcc};
248 {exception, Info} ->
249 8 ?MODULE:error_running_hook(Info, Handler, Acc, Params, Key),
250 8 run_hook(Ls, Acc, Params, Key)
251 end.
252
253 -spec apply_hook_function(#hook_handler{}, hook_acc(), hook_params()) ->
254 hook_fn_ret() | safely:exception().
255 apply_hook_function(#hook_handler{hook_fn = HookFn, extra = Extra},
256 Acc, Params) ->
257 533547 ?SAFELY(HookFn(Acc, Params, Extra)).
258
259 error_running_hook(Info, Handler, Acc, Params, Key) ->
260 8 ?LOG_ERROR(Info#{what => hook_failed,
261 text => <<"Error running hook">>,
262 key => Key,
263 handler => Handler,
264 acc => Acc,
265
:-(
params => Params}).
266
267 -spec make_hook_handler(hook_tuple()) -> #hook_handler{}.
268 make_hook_handler({HookName, Tag, Function, Extra, Priority} = HookTuple)
269 when is_atom(HookName), is_binary(Tag) or (Tag =:= global),
270 is_function(Function, 3), is_map(Extra),
271 is_integer(Priority), Priority > 0 ->
272 70844 NewExtra = extend_extra(HookTuple),
273 70844 check_hook_function(Function),
274 70844 #hook_handler{prio = Priority,
275 hook_fn = Function,
276 extra = NewExtra}.
277
278 -spec fun_is_handler_equal_to(#hook_handler{}) -> fun((#hook_handler{}) -> boolean()).
279 fun_is_handler_equal_to(#hook_handler{prio = P0, hook_fn = HookFn0, extra = Extra0}) ->
280 43607 Mod0 = erlang:fun_info(HookFn0, module),
281 43607 Name0 = erlang:fun_info(HookFn0, name),
282 43607 fun(#hook_handler{prio = P1, hook_fn = HookFn1, extra = Extra1}) ->
283 76048 P0 =:= P1 andalso Extra0 =:= Extra1 andalso
284 50557 Mod0 =:= erlang:fun_info(HookFn1, module) andalso
285 29036 Name0 =:= erlang:fun_info(HookFn1, name)
286 end.
287
288 -spec check_hook_function(hook_fn()) -> ok.
289 check_hook_function(Function) when is_function(Function, 3) ->
290 70844 case erlang:fun_info(Function, type) of
291 {type, external} ->
292 70844 {module, Module} = erlang:fun_info(Function, module),
293 70844 {name, FunctionName} = erlang:fun_info(Function, name),
294 70844 case code:ensure_loaded(Module) of
295 70844 {module, Module} -> ok;
296 Error ->
297
:-(
throw_error(#{what => module_is_not_loaded,
298 module => Module, error => Error})
299 end,
300 70844 case erlang:function_exported(Module, FunctionName, 3) of
301 70844 true -> ok;
302 false ->
303
:-(
throw_error(#{what => function_is_not_exported,
304 function => Function})
305 end;
306 {type, local} ->
307
:-(
throw_error(#{what => only_external_function_references_allowed,
308 function => Function})
309 end.
310
311 -spec throw_error(map()) -> no_return().
312 throw_error(ErrorMap) ->
313
:-(
error(ErrorMap).
314
315 -spec hook_key(HookName :: hook_name(), Tag :: hook_tag()) -> key().
316 hook_key(HookName, Tag) ->
317 563904 {HookName, Tag}.
318
319 -spec extend_extra(hook_tuple()) -> hook_extra().
320 extend_extra({HookName, Tag, _Function, OriginalExtra, _Priority}) ->
321 70844 ExtraExtension = case Tag of
322 754 global -> #{hook_name => HookName, hook_tag => Tag};
323 HostType when is_binary(HostType) ->
324 70090 #{hook_name => HookName, hook_tag => Tag,
325 host_type => HostType}
326 end,
327 %% KV pairs of the OriginalExtra map will remain unchanged,
328 %% only the new keys from the ExtraExtension map will be added
329 %% to the NewExtra map
330 70844 maps:merge(ExtraExtension, OriginalExtra).
Line Hits Source