./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
13 %% gen_server callbacks
14 -export([init/1,
15 handle_call/3,
16 handle_cast/2,
17 code_change/3,
18 handle_info/2,
19 terminate/2]).
20
21 %% exported for unit tests only
22 -export([error_running_hook/5]).
23
24 -ignore_xref([start_link/0, add_handlers/1, delete_handlers/1]).
25
26 -include("mongoose.hrl").
27
28 -type hook_name() :: atom().
29 -type hook_tag() :: mongoose:host_type() | global.
30
31 %% while Accumulator is not limited to any type, it's recommended to use maps.
32 -type hook_acc() :: any().
33 -type hook_params() :: map().
34 -type hook_extra() :: map().
35
36 -type hook_fn_ret_value() :: {ok | stop, NewAccumulator :: hook_acc()}.
37 -type hook_fn() :: %% see run_fold/4 documentation
38 fun((Accumulator :: hook_acc(),
39 ExecutionParameters :: hook_params(),
40 ExtraParameters :: hook_extra()) -> hook_fn_ret_value()).
41
42 -type key() :: {HookName :: atom(),
43 Tag :: any()}.
44
45 -type hook_tuple() :: {HookName :: hook_name(),
46 Tag :: hook_tag(),
47 Function :: hook_fn(),
48 Extra :: hook_extra(),
49 Priority :: pos_integer()}.
50
51 -type hook_list() :: [hook_tuple()].
52
53 -export_type([hook_fn/0, hook_list/0]).
54
55 -record(hook_handler, {prio :: pos_integer(),
56 hook_fn :: hook_fn(),
57 extra :: map()}).
58
59 -define(TABLE, ?MODULE).
60 %%%----------------------------------------------------------------------
61 %%% API
62 %%%----------------------------------------------------------------------
63 start_link() ->
64 76 gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
65
66 %% @doc Add a handler for a hook.
67 %% Priority is used to sort the calls (lower numbers are executed first).
68 -spec add_handler(HookName :: hook_name(),
69 Tag :: hook_tag(),
70 Function :: hook_fn(),
71 Extra :: hook_extra(),
72 Priority :: pos_integer()) -> ok.
73 add_handler(HookName, Tag, Function, Extra, Priority) ->
74 28159 add_handler({HookName, Tag, Function, Extra, Priority}).
75
76 -spec add_handlers(hook_list()) -> ok.
77 add_handlers(List) ->
78
:-(
[add_handler(HookTuple) || HookTuple <- List],
79
:-(
ok.
80
81 -spec add_handler(hook_tuple()) -> ok.
82 add_handler({HookName, Tag, _, _, _} = HookTuple) ->
83 28159 Handler = make_hook_handler(HookTuple),
84 28159 Key = hook_key(HookName, Tag),
85 28159 gen_server:call(?MODULE, {add_handler, Key, Handler}).
86
87 %% @doc Delete a hook handler.
88 %% It is important to indicate exactly the same information than when the call was added.
89 -spec delete_handler(HookName :: hook_name(),
90 Tag :: hook_tag(),
91 Function :: hook_fn(),
92 Extra :: hook_extra(),
93 Priority :: pos_integer()) -> ok.
94 delete_handler(HookName, Tag, Function, Extra, Priority) ->
95 17257 delete_handler({HookName, Tag, Function, Extra, Priority}).
96
97 -spec delete_handlers(hook_list()) -> ok.
98 delete_handlers(List) ->
99
:-(
[delete_handler(HookTuple) || HookTuple <- List],
100
:-(
ok.
101
102 -spec delete_handler(hook_tuple()) -> ok.
103 delete_handler({HookName, Tag, _, _, _} = HookTuple) ->
104 17257 Handler = make_hook_handler(HookTuple),
105 17257 Key = hook_key(HookName, Tag),
106 17257 gen_server:call(?MODULE, {delete_handler, Key, Handler}).
107
108 %% @doc Run hook handlers in order of priority (lower number means higher priority).
109 %% * if a hook handler returns {ok, NewAcc}, the NewAcc value is used
110 %% as an accumulator parameter for the following hook handler.
111 %% * if a hook handler returns {stop, NewAcc}, execution stops immediately
112 %% without invoking lower priority hook handlers.
113 %% * if a hook handler crashes, the error is logged and the next hook handler
114 %% is executed.
115 %% Note that every hook handler MUST return a valid Acc. If a hook handler is not
116 %% interested in changing Acc parameter (or even if Acc is not used for a hook
117 %% at all), it must return (pass through) an unchanged input accumulator value.
118 -spec run_fold(HookName :: hook_name(),
119 Tag :: hook_tag(),
120 Acc :: hook_acc(),
121 Params :: hook_params()) -> hook_fn_ret_value().
122 run_fold(HookName, Tag, Acc, Params) ->
123 348665 Key = hook_key(HookName, Tag),
124 348665 case ets:lookup(?TABLE, Key) of
125 [{_, Ls}] ->
126 270161 mongoose_metrics:increment_generic_hook_metric(Tag, HookName),
127 270161 run_hook(Ls, Acc, Params, Key);
128 [] ->
129 78504 {ok, Acc}
130 end.
131
132 %%%----------------------------------------------------------------------
133 %%% gen_server callback functions
134 %%%----------------------------------------------------------------------
135
136 init([]) ->
137 76 ets:new(?TABLE, [named_table, {read_concurrency, true}]),
138 76 {ok, no_state}.
139
140 handle_call({add_handler, Key, #hook_handler{} = HookHandler}, _From, State) ->
141 28159 Reply = case ets:lookup(?TABLE, Key) of
142 [{_, Ls}] ->
143 12698 case lists:search(fun_is_handler_equal_to(HookHandler), Ls) of
144 {value, _} ->
145 184 ?LOG_WARNING(#{what => duplicated_handler,
146
:-(
key => Key, handler => HookHandler}),
147 184 ok;
148 false ->
149 %% NOTE: sort *only* on the priority,
150 %% order of other fields is not part of the contract
151 12514 NewLs = lists:keymerge(#hook_handler.prio, Ls, [HookHandler]),
152 12514 ets:insert(?TABLE, {Key, NewLs}),
153 12514 ok
154 end;
155 [] ->
156 15461 NewLs = [HookHandler],
157 15461 ets:insert(?TABLE, {Key, NewLs}),
158 15461 create_hook_metric(Key),
159 15461 ok
160 end,
161 28159 {reply, Reply, State};
162 handle_call({delete_handler, Key, #hook_handler{} = HookHandler}, _From, State) ->
163 17257 Reply = case ets:lookup(?TABLE, Key) of
164 [{_, Ls}] ->
165 %% NOTE: The straightforward handlers comparison would compare
166 %% the function objects, which is not well-defined in OTP.
167 %% So we do a manual comparison on the MFA of the funs,
168 %% by using `erlang:fun_info/2`
169 17250 Pred = fun_is_handler_equal_to(HookHandler),
170 17250 {_, NewLs} = lists:partition(Pred, Ls),
171 17250 ets:insert(?TABLE, {Key, NewLs}),
172 17250 ok;
173 [] ->
174 7 ok
175 end,
176 17257 {reply, Reply, State};
177 handle_call(Request, From, State) ->
178
:-(
?UNEXPECTED_CALL(Request, From),
179
:-(
{reply, bad_request, State}.
180
181 handle_cast(Msg, State) ->
182
:-(
?UNEXPECTED_CAST(Msg),
183
:-(
{noreply, State}.
184
185 handle_info(Info, State) ->
186
:-(
?UNEXPECTED_INFO(Info),
187
:-(
{noreply, State}.
188
189 terminate(_Reason, _State) ->
190
:-(
ets:delete(?TABLE),
191
:-(
ok.
192
193 code_change(_OldVsn, State, _Extra) ->
194
:-(
{ok, State}.
195
196 %%%----------------------------------------------------------------------
197 %%% Internal functions
198 %%%----------------------------------------------------------------------
199 -spec run_hook([#hook_handler{}], hook_acc(), hook_params(), key()) -> hook_fn_ret_value().
200 run_hook([], Acc, _Params, _Key) ->
201 260931 {ok, Acc};
202 run_hook([Handler | Ls], Acc, Params, Key) ->
203 351583 case apply_hook_function(Handler, Acc, Params) of
204 {ok, NewAcc} ->
205 342333 run_hook(Ls, NewAcc, Params, Key);
206 {stop, NewAcc} ->
207 9230 {stop, NewAcc};
208 Other ->
209 20 ?MODULE:error_running_hook(Other, Handler, Acc, Params, Key),
210 20 run_hook(Ls, Acc, Params, Key)
211 end.
212
213 -spec apply_hook_function(#hook_handler{}, hook_acc(), hook_params()) ->
214 hook_fn_ret_value() | {'EXIT', Reason :: any()}.
215 apply_hook_function(#hook_handler{hook_fn = HookFn, extra = Extra},
216 Acc, Params) ->
217 351583 safely:apply(HookFn, [Acc, Params, Extra]).
218
219 error_running_hook({Class, Reason}, Handler, Acc, Params, Key) ->
220 20 Extra = #{class => Class, reason => Reason},
221 20 log_error_running_hook(Extra, Handler, Acc, Params, Key);
222 error_running_hook(Other, Handler, Acc, Params, Key) ->
223
:-(
Extra = #{error => Other},
224
:-(
log_error_running_hook(Extra, Handler, Acc, Params, Key).
225
226 log_error_running_hook(Extra, Handler, Acc, Params, Key) ->
227 20 ?LOG_ERROR(Extra#{what => hook_failed,
228 text => <<"Error running hook">>,
229 key => Key,
230 handler => Handler,
231 acc => Acc,
232
:-(
params => Params}).
233
234 -spec make_hook_handler(hook_tuple()) -> #hook_handler{}.
235 make_hook_handler({HookName, Tag, Function, Extra, Priority} = HookTuple)
236 when is_atom(HookName), is_binary(Tag) or (Tag =:= global),
237 is_function(Function, 3), is_map(Extra),
238 is_integer(Priority), Priority > 0 ->
239 45416 NewExtra = extend_extra(HookTuple),
240 45416 check_hook_function(Function),
241 45416 #hook_handler{prio = Priority,
242 hook_fn = Function,
243 extra = NewExtra}.
244
245 -spec fun_is_handler_equal_to(#hook_handler{}) -> fun((#hook_handler{}) -> boolean()).
246 fun_is_handler_equal_to(#hook_handler{prio = P0, hook_fn = HookFn0, extra = Extra0}) ->
247 29948 Mod0 = erlang:fun_info(HookFn0, module),
248 29948 Name0 = erlang:fun_info(HookFn0, name),
249 29948 fun(#hook_handler{prio = P1, hook_fn = HookFn1, extra = Extra1}) ->
250 53681 P0 =:= P1 andalso Extra0 =:= Extra1 andalso
251 17396 Mod0 =:= erlang:fun_info(HookFn1, module) andalso
252 17396 Name0 =:= erlang:fun_info(HookFn1, name)
253 end.
254
255 -spec check_hook_function(hook_fn()) -> ok.
256 check_hook_function(Function) when is_function(Function, 3) ->
257 45416 case erlang:fun_info(Function, type) of
258 {type, external} ->
259 45416 {module, Module} = erlang:fun_info(Function, module),
260 45416 {name, FunctionName} = erlang:fun_info(Function, name),
261 45416 case code:ensure_loaded(Module) of
262 45416 {module, Module} -> ok;
263 Error ->
264
:-(
throw_error(#{what => module_is_not_loaded,
265 module => Module, error => Error})
266 end,
267 45416 case erlang:function_exported(Module, FunctionName, 3) of
268 45416 true -> ok;
269 false ->
270
:-(
throw_error(#{what => function_is_not_exported,
271 function => Function})
272 end;
273 {type, local} ->
274
:-(
throw_error(#{what => only_external_function_references_allowed,
275 function => Function})
276 end.
277
278 -spec throw_error(map()) -> no_return().
279 throw_error(ErrorMap) ->
280
:-(
error(ErrorMap).
281
282 -spec hook_key(HookName :: hook_name(), Tag :: hook_tag()) -> key().
283 hook_key(HookName, Tag) ->
284 394081 {HookName, Tag}.
285
286 -spec extend_extra(hook_tuple()) -> hook_extra().
287 extend_extra({HookName, Tag, _Function, OriginalExtra, _Priority}) ->
288 45416 ExtraExtension = case Tag of
289 640 global -> #{hook_name => HookName, hook_tag => Tag};
290 HostType when is_binary(HostType) ->
291 44776 #{hook_name => HookName, hook_tag => Tag,
292 host_type => HostType}
293 end,
294 %% KV pairs of the OriginalExtra map will remain unchanged,
295 %% only the new keys from the ExtraExtension map will be added
296 %% to the NewExtra map
297 45416 maps:merge(ExtraExtension, OriginalExtra).
298
299 -spec create_hook_metric(Key :: key()) -> any().
300 create_hook_metric({HookName, Tag}) ->
301 15461 mongoose_metrics:create_generic_hook_metric(Tag, HookName).
Line Hits Source