./ct_report/coverage/gen_mod_deps.COVER.html

1 %%==============================================================================
2 %% Copyright 2016 Erlang Solutions Ltd.
3 %%
4 %% Licensed under the Apache License, Version 2.0 (the "License");
5 %% you may not use this file except in compliance with the License.
6 %% You may obtain a copy of the License at
7 %%
8 %% http://www.apache.org/licenses/LICENSE-2.0
9 %%
10 %% Unless required by applicable law or agreed to in writing, software
11 %% distributed under the License is distributed on an "AS IS" BASIS,
12 %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 %% See the License for the specific language governing permissions and
14 %% limitations under the License.
15 %%==============================================================================
16
17 -module(gen_mod_deps).
18
19 -include("mongoose.hrl").
20
21 -type hardness() :: soft | hard | optional.
22 -type module_opts() :: gen_mod:module_opts().
23 -type module_dep() :: {module(), module_opts(), hardness()}.
24 -type module_deps() :: [module_dep()].
25 -type deps() :: [module_dep() | {service, mongoose_service:service()}].
26 -type module_list() :: [{module(), module_opts()}].
27 -type module_map() :: #{module() => module_opts()}.
28
29 -export([add_deps/2, resolve_deps/2, sort_deps/2]).
30
31 -ignore_xref([add_deps/2]).
32
33 -export_type([hardness/0, module_list/0, module_map/0, module_deps/0, deps/0]).
34
35 %%--------------------------------------------------------------------
36 %% API
37 %%--------------------------------------------------------------------
38
39 %% @doc Adds deps into module list.
40 %% Side-effect free.
41 -spec add_deps(mongooseim:host_type(), module_map() | module_list()) -> module_list().
42 add_deps(HostType, Modules) ->
43
:-(
sort_deps(HostType, resolve_deps(HostType, Modules)).
44
45 %%--------------------------------------------------------------------
46 %% Helpers
47 %%--------------------------------------------------------------------
48
49 %% Resolving dependencies
50
51 %% @doc
52 %% Determines all modules to start, along with their options.
53 %%
54 %% NOTE: A dependency will not be discarded during resolving, e.g.
55 %% if the resolver processes dependencies in order:
56 %%
57 %% deps(mod_a, #{}) -> [{mod_b, #{}}, {mod_c, #{}}]
58 %% deps(mod_parent, #{}) -> [{mod_a, #{opt => val}}]
59 %%
60 %% then the dependency for mod_a will be reevaluated with new opts and might return:
61 %%
62 %% deps(mod_a, #{opt := val}) -> [{mod_c, #{}}]
63 %%
64 %% In this case, mod_b will still be started.
65 %% @end
66 -spec resolve_deps(mongooseim:host_type(), module_map() | module_list()) -> module_map().
67 resolve_deps(HostType, Modules) when is_map(Modules) ->
68 1151 resolve_deps(HostType, maps:to_list(Modules));
69 resolve_deps(HostType, ModuleQueue) ->
70 1151 resolve_deps(HostType, ModuleQueue, #{}, #{}).
71
72 -spec resolve_deps(mongooseim:host_type(),
73 ModuleQueue :: [{module(), module_opts()} | module_dep()],
74 OptionalMods :: module_map(),
75 Acc :: module_map()) -> module_map().
76 resolve_deps(HostType, [], OptionalMods, KnownModules) ->
77 1183 KnownModNames = maps:keys(KnownModules),
78 1183 case maps:with(KnownModNames, OptionalMods) of
79 NewQueueMap when map_size(NewQueueMap) > 0 ->
80 32 resolve_deps(HostType, maps:to_list(NewQueueMap),
81 maps:without(KnownModNames, OptionalMods), KnownModules);
82 _Nothing ->
83 1151 KnownModules
84 end;
85 resolve_deps(HostType, [{Module, Opts, optional} | ModuleQueue], OptionalMods, KnownModules) ->
86 92 resolve_deps(HostType, ModuleQueue, maps:put(Module, Opts, OptionalMods), KnownModules);
87 resolve_deps(HostType, [{Module, Opts, _Hardness} | ModuleQueue], OptionalMods, KnownModules) ->
88 742 resolve_deps(HostType, [{Module, Opts} | ModuleQueue], OptionalMods, KnownModules);
89 resolve_deps(HostType, [{Module, Opts} | ModuleQueue], OptionalMods, KnownModules)
90 when is_list(Opts); is_map(Opts) ->
91 12566 NewOpts =
92 case maps:find(Module, KnownModules) of
93 {ok, PreviousOpts} ->
94 500 case merge_opts(Module, PreviousOpts, Opts) of
95 492 PreviousOpts -> undefined;
96 8 MergedOpts -> MergedOpts
97 end;
98 error ->
99 12066 Opts
100 end,
101 12566 case NewOpts of
102 492 undefined -> resolve_deps(HostType, ModuleQueue, OptionalMods, KnownModules);
103 _ ->
104 12074 Deps = gen_mod:get_deps(HostType, Module, NewOpts),
105 12074 UpdatedQueue = Deps ++ ModuleQueue,
106 12074 UpdatedKnownModules = maps:put(Module, NewOpts, KnownModules),
107 12074 resolve_deps(HostType, UpdatedQueue, OptionalMods, UpdatedKnownModules)
108 end.
109
110 %% @doc
111 %% Merges module opts prioritizing the new ones, and warns on overrides.
112 %% @end
113 -spec merge_opts(module(), module_opts(), module_opts()) -> module_opts().
114 merge_opts(Module, PreviousOpts, Opts) when is_list(PreviousOpts), is_list(Opts) ->
115 %% Temporary clause, should be removed when all modules support maps
116 80 maps:to_list(merge_opts(Module, maps:from_list(proplists:unfold(PreviousOpts)),
117 maps:from_list(proplists:unfold(Opts))));
118 merge_opts(Module, PreviousOpts, Opts) when is_map(PreviousOpts), is_map(Opts) ->
119 500 case changed_opts(PreviousOpts, Opts) of
120 [] ->
121 500 ok;
122 Changed ->
123
:-(
?LOG_WARNING(#{what => overriding_options, module => Module, options => Changed})
124 end,
125 500 maps:merge(PreviousOpts, Opts).
126
127 -spec changed_opts(module_opts(), module_opts()) -> [map()].
128 changed_opts(PreviousOpts, Opts) ->
129 500 lists:flatmap(
130 fun({Key, OldValue}) ->
131 3377 case maps:find(Key, Opts) of
132
:-(
error -> [];
133 3377 {ok, OldValue} -> [];
134
:-(
{ok, NewValue} -> [#{key => Key, old_value => OldValue, new_value => NewValue}]
135 end
136 end, maps:to_list(PreviousOpts)).
137
138 %% Sorting resolved dependencies
139
140 -spec sort_deps(mongooseim:host_type(), module_map()) -> module_list().
141 sort_deps(HostType, ModuleMap) ->
142 1572 DepsGraph = digraph:new([acyclic, private]),
143
144 1572 try
145 1572 maps:fold(
146 fun(Module, Opts, _) ->
147 16103 process_module_dep(HostType, Module, Opts, DepsGraph)
148 end,
149 undefined, ModuleMap),
150
151 1572 lists:filtermap(
152 fun(Module) ->
153 16163 case maps:find(Module, ModuleMap) of
154 60 error -> false;
155 16103 {ok, Opts} -> {true, {Module, Opts}}
156 end
157 end,
158 digraph_utils:topsort(DepsGraph))
159 after
160 1572 digraph:delete(DepsGraph)
161 end.
162
163 -spec process_module_dep(mongooseim:host_type(), module(), module_opts(), digraph:graph()) -> ok.
164 process_module_dep(HostType, Module, Opts, DepsGraph) ->
165 16103 digraph:add_vertex(DepsGraph, Module),
166 16103 lists:foreach(
167 847 fun({DepModule, _, DepHardness}) -> process_dep(Module, DepModule, DepHardness, DepsGraph) end,
168 gen_mod:get_deps(HostType, Module, Opts)).
169
170 -spec process_dep(Module :: module(), DepModule :: module(),
171 DepHardness :: hardness(), Graph :: digraph:graph()) -> ok.
172 process_dep(Module, DepModule, DepHardness, Graph) ->
173 847 digraph:add_vertex(Graph, DepModule),
174 847 case {digraph:add_edge(Graph, DepModule, Module, DepHardness), DepHardness} of
175 {['$e' | _], _} ->
176 847 ok;
177
178 {{error, {bad_edge, CyclePath}}, hard} ->
179
:-(
case find_soft_edge(Graph, CyclePath) of
180 false ->
181
:-(
?LOG_CRITICAL(#{what => resolving_dependencies_aborted,
182 text => <<"Module dependency cycle found">>,
183
:-(
cycle_path => CyclePath}),
184
:-(
error({dependency_cycle, CyclePath});
185
186 {EdgeId, B, A, _} ->
187
:-(
?LOG_INFO(#{what => soft_module_dependency_cycle_detected,
188 text => <<"Soft module dependency cycle detected. "
189 "Dropping edge">>, edge => {A, B},
190
:-(
cyclepath => CyclePath}),
191
192
:-(
digraph:del_edge(Graph, EdgeId),
193
:-(
['$e' | _] = digraph:add_edge(Graph, DepModule, Module, hard),
194
:-(
ok
195 end;
196
197 {{error, {bad_edge, CyclePath}}, _Soft} ->
198
:-(
?LOG_INFO(#{what => soft_module_dependency_cycle_detected,
199 text => <<"Soft module dependency cycle detected. "
200 "Dropping edge">>, edge => {Module, DepModule},
201
:-(
cyclepath => CyclePath}),
202
:-(
ok
203 end.
204
205 -spec find_soft_edge(digraph:graph(), [digraph:vertex()]) ->
206 {digraph:edge(), digraph:vertex(),
207 digraph:vertex(), digraph:label()} | false.
208 find_soft_edge(Graph, CyclePath) ->
209
:-(
VerticePairs = lists:zip(CyclePath, tl(CyclePath) ++ [hd(CyclePath)]),
210
:-(
Edges = lists:filtermap(
211 fun({A, B}) ->
212
:-(
case find_edge(Graph, A, B) of
213
:-(
false -> false;
214
:-(
Edge -> {true, digraph:edge(Graph, Edge)}
215 end
216 end,
217 VerticePairs),
218
219
:-(
case lists:keyfind(optional, 4, Edges) of
220
:-(
false -> lists:keyfind(soft, 4, Edges);
221
:-(
Edge -> Edge
222 end.
223
224 -spec find_edge(digraph:graph(), digraph:vertex(), digraph:vertex()) -> digraph:edge() | false.
225 find_edge(Graph, A, B) ->
226
:-(
OutEdges = ordsets:from_list(digraph:out_edges(Graph, A)),
227
:-(
InEdges = ordsets:from_list(digraph:in_edges(Graph, B)),
228
229
:-(
case ordsets:intersection(OutEdges, InEdges) of
230
:-(
[Edge] -> Edge;
231
:-(
[] -> false
232 end.
Line Hits Source