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 |
82 |
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 |
26481 |
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 |
26481 |
Handler = make_hook_handler(HookTuple), |
84 |
26481 |
Key = hook_key(HookName, Tag), |
85 |
26481 |
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 |
14953 |
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 |
14953 |
Handler = make_hook_handler(HookTuple), |
105 |
14953 |
Key = hook_key(HookName, Tag), |
106 |
14953 |
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 |
183965 |
Key = hook_key(HookName, Tag), |
124 |
183965 |
case ets:lookup(?TABLE, Key) of |
125 |
|
[{_, Ls}] -> |
126 |
145934 |
mongoose_metrics:increment_generic_hook_metric(Tag, HookName), |
127 |
145934 |
run_hook(Ls, Acc, Params, Key); |
128 |
|
[] -> |
129 |
38031 |
{ok, Acc} |
130 |
|
end. |
131 |
|
|
132 |
|
%%%---------------------------------------------------------------------- |
133 |
|
%%% gen_server callback functions |
134 |
|
%%%---------------------------------------------------------------------- |
135 |
|
|
136 |
|
init([]) -> |
137 |
82 |
ets:new(?TABLE, [named_table, {read_concurrency, true}]), |
138 |
82 |
{ok, no_state}. |
139 |
|
|
140 |
|
handle_call({add_handler, Key, #hook_handler{} = HookHandler}, _From, State) -> |
141 |
26481 |
Reply = case ets:lookup(?TABLE, Key) of |
142 |
|
[{_, Ls}] -> |
143 |
10253 |
case lists:search(fun_is_handler_equal_to(HookHandler), Ls) of |
144 |
|
{value, _} -> |
145 |
84 |
?LOG_WARNING(#{what => duplicated_handler, |
146 |
:-( |
key => Key, handler => HookHandler}), |
147 |
84 |
ok; |
148 |
|
false -> |
149 |
|
%% NOTE: sort *only* on the priority, |
150 |
|
%% order of other fields is not part of the contract |
151 |
10169 |
NewLs = lists:keymerge(#hook_handler.prio, Ls, [HookHandler]), |
152 |
10169 |
ets:insert(?TABLE, {Key, NewLs}), |
153 |
10169 |
ok |
154 |
|
end; |
155 |
|
[] -> |
156 |
16228 |
NewLs = [HookHandler], |
157 |
16228 |
ets:insert(?TABLE, {Key, NewLs}), |
158 |
16228 |
create_hook_metric(Key), |
159 |
16228 |
ok |
160 |
|
end, |
161 |
26481 |
{reply, Reply, State}; |
162 |
|
handle_call({delete_handler, Key, #hook_handler{} = HookHandler}, _From, State) -> |
163 |
14953 |
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 |
14951 |
Pred = fun_is_handler_equal_to(HookHandler), |
170 |
14951 |
{_, NewLs} = lists:partition(Pred, Ls), |
171 |
14951 |
ets:insert(?TABLE, {Key, NewLs}), |
172 |
14951 |
ok; |
173 |
|
[] -> |
174 |
2 |
ok |
175 |
|
end, |
176 |
14953 |
{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 |
145266 |
{ok, Acc}; |
202 |
|
run_hook([Handler | Ls], Acc, Params, Key) -> |
203 |
159412 |
case apply_hook_function(Handler, Acc, Params) of |
204 |
|
{ok, NewAcc} -> |
205 |
158698 |
run_hook(Ls, NewAcc, Params, Key); |
206 |
|
{stop, NewAcc} -> |
207 |
668 |
{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 |
159412 |
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 |
41434 |
NewExtra = extend_extra(HookTuple), |
240 |
41434 |
check_hook_function(Function), |
241 |
41434 |
#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 |
25204 |
Mod0 = erlang:fun_info(HookFn0, module), |
248 |
25204 |
Name0 = erlang:fun_info(HookFn0, name), |
249 |
25204 |
fun(#hook_handler{prio = P1, hook_fn = HookFn1, extra = Extra1}) -> |
250 |
44216 |
P0 =:= P1 andalso Extra0 =:= Extra1 andalso |
251 |
15031 |
Mod0 =:= erlang:fun_info(HookFn1, module) andalso |
252 |
15031 |
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 |
41434 |
case erlang:fun_info(Function, type) of |
258 |
|
{type, external} -> |
259 |
41434 |
{module, Module} = erlang:fun_info(Function, module), |
260 |
41434 |
{name, FunctionName} = erlang:fun_info(Function, name), |
261 |
41434 |
case code:ensure_loaded(Module) of |
262 |
41434 |
{module, Module} -> ok; |
263 |
|
Error -> |
264 |
:-( |
throw_error(#{what => module_is_not_loaded, |
265 |
|
module => Module, error => Error}) |
266 |
|
end, |
267 |
41434 |
case erlang:function_exported(Module, FunctionName, 3) of |
268 |
41434 |
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 |
225399 |
{HookName, Tag}. |
285 |
|
|
286 |
|
-spec extend_extra(hook_tuple()) -> hook_extra(). |
287 |
|
extend_extra({HookName, Tag, _Function, OriginalExtra, _Priority}) -> |
288 |
41434 |
ExtraExtension = case Tag of |
289 |
826 |
global -> #{hook_name => HookName, hook_tag => Tag}; |
290 |
|
HostType when is_binary(HostType) -> |
291 |
40608 |
#{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 |
41434 |
maps:merge(ExtraExtension, OriginalExtra). |
298 |
|
|
299 |
|
-spec create_hook_metric(Key :: key()) -> any(). |
300 |
|
create_hook_metric({HookName, Tag}) -> |
301 |
16228 |
mongoose_metrics:create_generic_hook_metric(Tag, HookName). |