./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_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 33 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 84 add_handler({HookName, Tag, Function, Extra, Priority}).
89
90 -spec add_handlers(hook_list()) -> ok.
91 add_handlers(List) ->
92 3280 [add_handler(HookTuple) || HookTuple <- List],
93 3280 ok.
94
95 -spec add_handler(hook_tuple()) -> ok.
96 add_handler({HookName, Tag, _, _, _} = HookTuple) ->
97 22325 Handler = make_hook_handler(HookTuple),
98 22325 Key = hook_key(HookName, Tag),
99 22325 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 51 delete_handler({HookName, Tag, Function, Extra, Priority}).
110
111 -spec delete_handlers(hook_list()) -> ok.
112 delete_handlers(List) ->
113 2628 [delete_handler(HookTuple) || HookTuple <- List],
114 2628 ok.
115
116 -spec delete_handler(hook_tuple()) -> ok.
117 delete_handler({HookName, Tag, _, _, _} = HookTuple) ->
118 17030 Handler = make_hook_handler(HookTuple),
119 17030 Key = hook_key(HookName, Tag),
120 17030 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 469327 Key = hook_key(HookName, Tag),
138 469327 case persistent_term:get(?MODULE, #{}) of
139 #{Key := Ls} ->
140 384358 mongoose_metrics:increment_generic_hook_metric(Tag, HookName),
141 384358 run_hook(Ls, Acc, Params, Key);
142 _ ->
143 84969 {ok, Acc}
144 end.
145
146 reload_hooks() ->
147 33 gen_server:call(?MODULE, reload_hooks).
148
149 %%%----------------------------------------------------------------------
150 %%% gen_server callback functions
151 %%%----------------------------------------------------------------------
152
153 init([]) ->
154 33 erlang:process_flag(trap_exit, true), %% We need to make sure that terminate is called in tests
155 33 {ok, #{}}.
156
157 handle_call({add_handler, Key, #hook_handler{} = HookHandler}, _From, State) ->
158 22325 NewState =
159 case maps:get(Key, State, []) of
160 [] ->
161 12807 NewLs = [HookHandler],
162 12807 create_hook_metric(Key),
163 12807 maps:put(Key, NewLs, State);
164 Ls ->
165 9518 case lists:search(fun_is_handler_equal_to(HookHandler), Ls) of
166 {value, _} ->
167 116 ?LOG_WARNING(#{what => duplicated_handler,
168
:-(
key => Key, handler => HookHandler}),
169 116 State;
170 false ->
171 %% NOTE: sort *only* on the priority,
172 %% order of other fields is not part of the contract
173 9402 NewLs = lists:keymerge(#hook_handler.prio, Ls, [HookHandler]),
174 9402 maps:put(Key, NewLs, State)
175 end
176 end,
177 22325 maybe_insert_immediately(NewState),
178 22325 {reply, ok, NewState};
179 handle_call({delete_handler, Key, #hook_handler{} = HookHandler}, _From, State) ->
180 17030 NewState =
181 case maps:get(Key, State, []) of
182 [] ->
183 65 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 16965 Pred = fun_is_handler_equal_to(HookHandler),
190 16965 {_, NewLs} = lists:partition(Pred, Ls),
191 16965 maps:put(Key, NewLs, State)
192 end,
193 17030 maybe_insert_immediately(NewState),
194 17030 {reply, ok, NewState};
195 handle_call(reload_hooks, _From, State) ->
196 33 persistent_term:put(?MODULE, State),
197 33 {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 34 persistent_term:erase(?MODULE),
212 34 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 39355 case persistent_term:get(?MODULE, hooks_not_set) of
227 hooks_not_set ->
228 13921 ok;
229 _ ->
230 25434 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 366130 {ok, Acc};
236 run_hook([Handler | Ls], Acc, Params, Key) ->
237 521283 case apply_hook_function(Handler, Acc, Params) of
238 {ok, NewAcc} ->
239 503046 run_hook(Ls, NewAcc, Params, Key);
240 {stop, NewAcc} ->
241 18228 {stop, NewAcc};
242 {exception, Info} ->
243 9 ?MODULE:error_running_hook(Info, Handler, Acc, Params, Key),
244 9 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 521283 ?SAFELY(HookFn(Acc, Params, Extra)).
252
253 error_running_hook(Info, Handler, Acc, Params, Key) ->
254 9 ?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 39355 NewExtra = extend_extra(HookTuple),
267 39355 check_hook_function(Function),
268 39355 #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 26483 Mod0 = erlang:fun_info(HookFn0, module),
275 26483 Name0 = erlang:fun_info(HookFn0, name),
276 26483 fun(#hook_handler{prio = P1, hook_fn = HookFn1, extra = Extra1}) ->
277 52371 P0 =:= P1 andalso Extra0 =:= Extra1 andalso
278 33517 Mod0 =:= erlang:fun_info(HookFn1, module) andalso
279 16921 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 39355 case erlang:fun_info(Function, type) of
285 {type, external} ->
286 39355 {module, Module} = erlang:fun_info(Function, module),
287 39355 {name, FunctionName} = erlang:fun_info(Function, name),
288 39355 case code:ensure_loaded(Module) of
289 39355 {module, Module} -> ok;
290 Error ->
291
:-(
throw_error(#{what => module_is_not_loaded,
292 module => Module, error => Error})
293 end,
294 39355 case erlang:function_exported(Module, FunctionName, 3) of
295 39355 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 508682 {HookName, Tag}.
312
313 -spec extend_extra(hook_tuple()) -> hook_extra().
314 extend_extra({HookName, Tag, _Function, OriginalExtra, _Priority}) ->
315 39355 ExtraExtension = case Tag of
316 433 global -> #{hook_name => HookName, hook_tag => Tag};
317 HostType when is_binary(HostType) ->
318 38922 #{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 39355 maps:merge(ExtraExtension, OriginalExtra).
325
326 -spec create_hook_metric(Key :: key()) -> any().
327 create_hook_metric({HookName, Tag}) ->
328 12807 mongoose_metrics:create_generic_hook_metric(Tag, HookName).
Line Hits Source