./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 80 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 26091 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 26091 Handler = make_hook_handler(HookTuple),
84 26091 Key = hook_key(HookName, Tag),
85 26091 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 14657 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 14657 Handler = make_hook_handler(HookTuple),
105 14657 Key = hook_key(HookName, Tag),
106 14657 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 178717 Key = hook_key(HookName, Tag),
124 178717 case ets:lookup(?TABLE, Key) of
125 [{_, Ls}] ->
126 141802 mongoose_metrics:increment_generic_hook_metric(Tag, HookName),
127 141802 run_hook(Ls, Acc, Params, Key);
128 [] ->
129 36915 {ok, Acc}
130 end.
131
132 %%%----------------------------------------------------------------------
133 %%% gen_server callback functions
134 %%%----------------------------------------------------------------------
135
136 init([]) ->
137 80 ets:new(?TABLE, [named_table, {read_concurrency, true}]),
138 80 {ok, no_state}.
139
140 handle_call({add_handler, Key, #hook_handler{} = HookHandler}, _From, State) ->
141 26091 Reply = case ets:lookup(?TABLE, Key) of
142 [{_, Ls}] ->
143 10315 case lists:search(fun_is_handler_equal_to(HookHandler), Ls) of
144 {value, _} ->
145 316 ?LOG_WARNING(#{what => duplicated_handler,
146
:-(
key => Key, handler => HookHandler}),
147 316 ok;
148 false ->
149 %% NOTE: sort *only* on the priority,
150 %% order of other fields is not part of the contract
151 9999 NewLs = lists:keymerge(#hook_handler.prio, Ls, [HookHandler]),
152 9999 ets:insert(?TABLE, {Key, NewLs}),
153 9999 ok
154 end;
155 [] ->
156 15776 NewLs = [HookHandler],
157 15776 ets:insert(?TABLE, {Key, NewLs}),
158 15776 create_hook_metric(Key),
159 15776 ok
160 end,
161 26091 {reply, Reply, State};
162 handle_call({delete_handler, Key, #hook_handler{} = HookHandler}, _From, State) ->
163 14657 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 14655 Pred = fun_is_handler_equal_to(HookHandler),
170 14655 {_, NewLs} = lists:partition(Pred, Ls),
171 14655 ets:insert(?TABLE, {Key, NewLs}),
172 14655 ok;
173 [] ->
174 2 ok
175 end,
176 14657 {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 141189 {ok, Acc};
202 run_hook([Handler | Ls], Acc, Params, Key) ->
203 155061 case apply_hook_function(Handler, Acc, Params) of
204 {ok, NewAcc} ->
205 154402 run_hook(Ls, NewAcc, Params, Key);
206 {stop, NewAcc} ->
207 611 {stop, NewAcc};
208 Other ->
209 46 ?MODULE:error_running_hook(Other, Handler, Acc, Params, Key),
210 46 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 155061 safely:apply(HookFn, [Acc, Params, Extra]).
218
219 error_running_hook({Class, Reason}, Handler, Acc, Params, Key) ->
220 46 Extra = #{class => Class, reason => Reason},
221 46 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 46 ?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 40748 NewExtra = extend_extra(HookTuple),
240 40748 check_hook_function(Function),
241 40748 #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 24970 Mod0 = erlang:fun_info(HookFn0, module),
248 24970 Name0 = erlang:fun_info(HookFn0, name),
249 24970 fun(#hook_handler{prio = P1, hook_fn = HookFn1, extra = Extra1}) ->
250 44204 P0 =:= P1 andalso Extra0 =:= Extra1 andalso
251 14967 Mod0 =:= erlang:fun_info(HookFn1, module) andalso
252 14967 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 40748 case erlang:fun_info(Function, type) of
258 {type, external} ->
259 40748 {module, Module} = erlang:fun_info(Function, module),
260 40748 {name, FunctionName} = erlang:fun_info(Function, name),
261 40748 case code:ensure_loaded(Module) of
262 40748 {module, Module} -> ok;
263 Error ->
264
:-(
throw_error(#{what => module_is_not_loaded,
265 module => Module, error => Error})
266 end,
267 40748 case erlang:function_exported(Module, FunctionName, 3) of
268 40748 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 219465 {HookName, Tag}.
285
286 -spec extend_extra(hook_tuple()) -> hook_extra().
287 extend_extra({HookName, Tag, _Function, OriginalExtra, _Priority}) ->
288 40748 ExtraExtension = case Tag of
289 1044 global -> #{hook_name => HookName, hook_tag => Tag};
290 HostType when is_binary(HostType) ->
291 39704 #{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 40748 maps:merge(ExtraExtension, OriginalExtra).
298
299 -spec create_hook_metric(Key :: key()) -> any().
300 create_hook_metric({HookName, Tag}) ->
301 15776 mongoose_metrics:create_generic_hook_metric(Tag, HookName).
Line Hits Source