./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 104 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 151 add_handler({HookName, Tag, Function, Extra, Priority}).
91
92 -spec add_handlers(hook_list()) -> ok.
93 add_handlers(List) ->
94 7533 [add_handler(HookTuple) || HookTuple <- List],
95 7533 ok.
96
97 -spec add_handler(hook_tuple()) -> ok.
98 add_handler({HookName, Tag, _, _, _} = HookTuple) ->
99 51115 Handler = make_hook_handler(HookTuple),
100 51115 Key = hook_key(HookName, Tag),
101 51115 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 47 delete_handler({HookName, Tag, Function, Extra, Priority}).
112
113 -spec delete_handlers(hook_list()) -> ok.
114 delete_handlers(List) ->
115 5533 [delete_handler(HookTuple) || HookTuple <- List],
116 5533 ok.
117
118 -spec delete_handler(hook_tuple()) -> ok.
119 delete_handler({HookName, Tag, _, _, _} = HookTuple) ->
120 35489 Handler = make_hook_handler(HookTuple),
121 35489 Key = hook_key(HookName, Tag),
122 35489 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 589073 Key = hook_key(HookName, Tag),
140 589073 case persistent_term:get(?MODULE, #{}) of
141 #{Key := Ls} ->
142 482406 mongoose_metrics:increment_generic_hook_metric(Tag, HookName),
143 482406 run_hook(Ls, Acc, Params, Key);
144 _ ->
145 106667 {ok, Acc}
146 end.
147
148 reload_hooks() ->
149 104 gen_server:call(?MODULE, reload_hooks).
150
151 %%%----------------------------------------------------------------------
152 %%% gen_server callback functions
153 %%%----------------------------------------------------------------------
154
155 init([]) ->
156 104 erlang:process_flag(trap_exit, true), %% We need to make sure that terminate is called in tests
157 104 {ok, #{}}.
158
159 handle_call({add_handler, Key, #hook_handler{} = HookHandler}, _From, State) ->
160 51115 NewState =
161 case maps:get(Key, State, []) of
162 [] ->
163 32304 NewLs = [HookHandler],
164 32304 create_hook_metric(Key),
165 32304 maps:put(Key, NewLs, State);
166 Ls ->
167 18811 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 18695 NewLs = lists:keymerge(#hook_handler.prio, Ls, [HookHandler]),
176 18695 maps:put(Key, NewLs, State)
177 end
178 end,
179 51115 maybe_insert_immediately(NewState),
180 51115 {reply, ok, NewState};
181 handle_call({delete_handler, Key, #hook_handler{} = HookHandler}, _From, State) ->
182 35489 NewState =
183 case maps:get(Key, State, []) of
184 [] ->
185 65 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 35424 Pred = fun_is_handler_equal_to(HookHandler),
192 35424 {_, NewLs} = lists:partition(Pred, Ls),
193 35424 maps:put(Key, NewLs, State)
194 end,
195 35489 maybe_insert_immediately(NewState),
196 35489 {reply, ok, NewState};
197 handle_call(reload_hooks, _From, State) ->
198 104 persistent_term:put(?MODULE, State),
199 104 {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 105 persistent_term:erase(?MODULE),
214 105 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 86604 case persistent_term:get(?MODULE, hooks_not_set) of
229 hooks_not_set ->
230 41916 ok;
231 _ ->
232 44688 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 455780 {ok, Acc};
238 run_hook([Handler | Ls], Acc, Params, Key) ->
239 671728 case apply_hook_function(Handler, Acc, Params) of
240 {ok, NewAcc} ->
241 645093 run_hook(Ls, NewAcc, Params, Key);
242 {stop, NewAcc} ->
243 26626 {stop, NewAcc};
244 {exception, Info} ->
245 9 ?MODULE:error_running_hook(Info, Handler, Acc, Params, Key),
246 9 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 671728 ?SAFELY(HookFn(Acc, Params, Extra)).
254
255 error_running_hook(Info, Handler, Acc, Params, Key) ->
256 9 ?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 86604 NewExtra = extend_extra(HookTuple),
269 86604 check_hook_function(Function),
270 86604 #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 54235 Mod0 = erlang:fun_info(HookFn0, module),
277 54235 Name0 = erlang:fun_info(HookFn0, name),
278 54235 fun(#hook_handler{prio = P1, hook_fn = HookFn1, extra = Extra1}) ->
279 95955 P0 =:= P1 andalso Extra0 =:= Extra1 andalso
280 62351 Mod0 =:= erlang:fun_info(HookFn1, module) andalso
281 35167 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 86604 case erlang:fun_info(Function, type) of
287 {type, external} ->
288 86604 {module, Module} = erlang:fun_info(Function, module),
289 86604 {name, FunctionName} = erlang:fun_info(Function, name),
290 86604 case code:ensure_loaded(Module) of
291 86604 {module, Module} -> ok;
292 Error ->
293
:-(
throw_error(#{what => module_is_not_loaded,
294 module => Module, error => Error})
295 end,
296 86604 case erlang:function_exported(Module, FunctionName, 3) of
297 86604 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 675677 {HookName, Tag}.
314
315 -spec extend_extra(hook_tuple()) -> hook_extra().
316 extend_extra({HookName, Tag, _Function, OriginalExtra, _Priority}) ->
317 86604 ExtraExtension = case Tag of
318 1002 global -> #{hook_name => HookName, hook_tag => Tag};
319 HostType when is_binary(HostType) ->
320 85602 #{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 86604 maps:merge(ExtraExtension, OriginalExtra).
327
328 -spec create_hook_metric(Key :: key()) -> any().
329 create_hook_metric({HookName, Tag}) ->
330 32304 mongoose_metrics:create_generic_hook_metric(Tag, HookName).
Line Hits Source