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 |
1381 |
resolve_deps(HostType, lists:sort(maps:to_list(Modules))); |
69 |
|
resolve_deps(HostType, ModuleQueue) -> |
70 |
1381 |
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 |
1416 |
KnownModNames = maps:keys(KnownModules), |
78 |
1416 |
case maps:with(KnownModNames, OptionalMods) of |
79 |
|
NewQueueMap when map_size(NewQueueMap) > 0 -> |
80 |
35 |
resolve_deps(HostType, maps:to_list(NewQueueMap), |
81 |
|
maps:without(KnownModNames, OptionalMods), KnownModules); |
82 |
|
_Nothing -> |
83 |
1381 |
KnownModules |
84 |
|
end; |
85 |
|
resolve_deps(HostType, [{Module, Opts, optional} | ModuleQueue], OptionalMods, KnownModules) -> |
86 |
97 |
resolve_deps(HostType, ModuleQueue, maps:put(Module, Opts, OptionalMods), KnownModules); |
87 |
|
resolve_deps(HostType, [{Module, Opts, _Hardness} | ModuleQueue], OptionalMods, KnownModules) -> |
88 |
702 |
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 |
12754 |
NewOpts = |
92 |
|
case maps:find(Module, KnownModules) of |
93 |
|
{ok, PreviousOpts} -> |
94 |
488 |
case merge_opts(Module, PreviousOpts, Opts) of |
95 |
488 |
PreviousOpts -> undefined; |
96 |
:-( |
MergedOpts -> MergedOpts |
97 |
|
end; |
98 |
|
error -> |
99 |
12266 |
Opts |
100 |
|
end, |
101 |
12754 |
case NewOpts of |
102 |
488 |
undefined -> resolve_deps(HostType, ModuleQueue, OptionalMods, KnownModules); |
103 |
|
_ -> |
104 |
12266 |
Deps = gen_mod:get_deps(HostType, Module, NewOpts), |
105 |
12266 |
UpdatedQueue = Deps ++ ModuleQueue, |
106 |
12266 |
UpdatedKnownModules = maps:put(Module, NewOpts, KnownModules), |
107 |
12266 |
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_map(PreviousOpts), is_map(Opts) -> |
115 |
488 |
case changed_opts(PreviousOpts, Opts) of |
116 |
|
[] -> |
117 |
488 |
ok; |
118 |
|
Changed -> |
119 |
:-( |
?LOG_WARNING(#{what => overriding_options, module => Module, options => Changed}) |
120 |
|
end, |
121 |
488 |
maps:merge(PreviousOpts, Opts). |
122 |
|
|
123 |
|
-spec changed_opts(module_opts(), module_opts()) -> [map()]. |
124 |
|
changed_opts(PreviousOpts, Opts) -> |
125 |
488 |
lists:flatmap( |
126 |
|
fun({Key, OldValue}) -> |
127 |
3537 |
case maps:find(Key, Opts) of |
128 |
35 |
error -> []; |
129 |
3502 |
{ok, OldValue} -> []; |
130 |
:-( |
{ok, NewValue} -> [#{key => Key, old_value => OldValue, new_value => NewValue}] |
131 |
|
end |
132 |
|
end, maps:to_list(PreviousOpts)). |
133 |
|
|
134 |
|
%% Sorting resolved dependencies |
135 |
|
|
136 |
|
-spec sort_deps(mongooseim:host_type(), module_map()) -> module_list(). |
137 |
|
sort_deps(HostType, ModuleMap) -> |
138 |
1906 |
DepsGraph = digraph:new([acyclic, private]), |
139 |
|
|
140 |
1906 |
try |
141 |
1906 |
maps:fold( |
142 |
|
fun(Module, Opts, _) -> |
143 |
16602 |
process_module_dep(HostType, Module, Opts, DepsGraph) |
144 |
|
end, |
145 |
|
undefined, ModuleMap), |
146 |
|
|
147 |
1906 |
lists:filtermap( |
148 |
|
fun(Module) -> |
149 |
16664 |
case maps:find(Module, ModuleMap) of |
150 |
62 |
error -> false; |
151 |
16602 |
{ok, Opts} -> {true, {Module, Opts}} |
152 |
|
end |
153 |
|
end, |
154 |
|
digraph_utils:topsort(DepsGraph)) |
155 |
|
after |
156 |
1906 |
digraph:delete(DepsGraph) |
157 |
|
end. |
158 |
|
|
159 |
|
-spec process_module_dep(mongooseim:host_type(), module(), module_opts(), digraph:graph()) -> ok. |
160 |
|
process_module_dep(HostType, Module, Opts, DepsGraph) -> |
161 |
16602 |
digraph:add_vertex(DepsGraph, Module), |
162 |
16602 |
lists:foreach( |
163 |
812 |
fun({DepModule, _, DepHardness}) -> process_dep(Module, DepModule, DepHardness, DepsGraph) end, |
164 |
|
gen_mod:get_deps(HostType, Module, Opts)). |
165 |
|
|
166 |
|
-spec process_dep(Module :: module(), DepModule :: module(), |
167 |
|
DepHardness :: hardness(), Graph :: digraph:graph()) -> ok. |
168 |
|
process_dep(Module, DepModule, DepHardness, Graph) -> |
169 |
812 |
digraph:add_vertex(Graph, DepModule), |
170 |
812 |
case {digraph:add_edge(Graph, DepModule, Module, DepHardness), DepHardness} of |
171 |
|
{['$e' | _], _} -> |
172 |
812 |
ok; |
173 |
|
|
174 |
|
{{error, {bad_edge, CyclePath}}, hard} -> |
175 |
:-( |
case find_soft_edge(Graph, CyclePath) of |
176 |
|
false -> |
177 |
:-( |
?LOG_CRITICAL(#{what => resolving_dependencies_aborted, |
178 |
|
text => <<"Module dependency cycle found">>, |
179 |
:-( |
cycle_path => CyclePath}), |
180 |
:-( |
error({dependency_cycle, CyclePath}); |
181 |
|
|
182 |
|
{EdgeId, B, A, _} -> |
183 |
:-( |
?LOG_INFO(#{what => soft_module_dependency_cycle_detected, |
184 |
|
text => <<"Soft module dependency cycle detected. " |
185 |
|
"Dropping edge">>, edge => {A, B}, |
186 |
:-( |
cyclepath => CyclePath}), |
187 |
|
|
188 |
:-( |
digraph:del_edge(Graph, EdgeId), |
189 |
:-( |
['$e' | _] = digraph:add_edge(Graph, DepModule, Module, hard), |
190 |
:-( |
ok |
191 |
|
end; |
192 |
|
|
193 |
|
{{error, {bad_edge, CyclePath}}, _Soft} -> |
194 |
:-( |
?LOG_INFO(#{what => soft_module_dependency_cycle_detected, |
195 |
|
text => <<"Soft module dependency cycle detected. " |
196 |
|
"Dropping edge">>, edge => {Module, DepModule}, |
197 |
:-( |
cyclepath => CyclePath}), |
198 |
:-( |
ok |
199 |
|
end. |
200 |
|
|
201 |
|
-spec find_soft_edge(digraph:graph(), [digraph:vertex()]) -> |
202 |
|
{digraph:edge(), digraph:vertex(), |
203 |
|
digraph:vertex(), digraph:label()} | false. |
204 |
|
find_soft_edge(Graph, CyclePath) -> |
205 |
:-( |
VerticePairs = lists:zip(CyclePath, tl(CyclePath) ++ [hd(CyclePath)]), |
206 |
:-( |
Edges = lists:filtermap( |
207 |
|
fun({A, B}) -> |
208 |
:-( |
case find_edge(Graph, A, B) of |
209 |
:-( |
false -> false; |
210 |
:-( |
Edge -> {true, digraph:edge(Graph, Edge)} |
211 |
|
end |
212 |
|
end, |
213 |
|
VerticePairs), |
214 |
|
|
215 |
:-( |
case lists:keyfind(optional, 4, Edges) of |
216 |
:-( |
false -> lists:keyfind(soft, 4, Edges); |
217 |
:-( |
Edge -> Edge |
218 |
|
end. |
219 |
|
|
220 |
|
-spec find_edge(digraph:graph(), digraph:vertex(), digraph:vertex()) -> digraph:edge() | false. |
221 |
|
find_edge(Graph, A, B) -> |
222 |
:-( |
OutEdges = ordsets:from_list(digraph:out_edges(Graph, A)), |
223 |
:-( |
InEdges = ordsets:from_list(digraph:in_edges(Graph, B)), |
224 |
|
|
225 |
:-( |
case ordsets:intersection(OutEdges, InEdges) of |
226 |
:-( |
[Edge] -> Edge; |
227 |
:-( |
[] -> false |
228 |
|
end. |