1 |
|
%%============================================================================== |
2 |
|
%% Copyright 2018 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(mongoose_service). |
18 |
|
|
19 |
|
-include("mongoose.hrl"). |
20 |
|
|
21 |
|
%% API |
22 |
|
-export([start/0, |
23 |
|
stop/0, |
24 |
|
config_spec/1, |
25 |
|
get_deps/1, |
26 |
|
is_loaded/1, |
27 |
|
assert_loaded/1]). |
28 |
|
|
29 |
|
%% Shell utilities |
30 |
|
-export([loaded_services_with_opts/0]). |
31 |
|
|
32 |
|
%% Service management utilities for tests |
33 |
|
-export([replace_services/2, ensure_stopped/1, ensure_started/2]). |
34 |
|
|
35 |
|
-ignore_xref([loaded_services_with_opts/0, replace_services/2, ensure_stopped/1, ensure_started/2]). |
36 |
|
|
37 |
|
-type service() :: module(). |
38 |
|
-type opt_key() :: atom(). |
39 |
|
-type opt_value() :: mongoose_config:value(). |
40 |
|
-type options() :: #{opt_key() => opt_value()}. |
41 |
|
-type start_result() :: any(). |
42 |
|
-type service_list() :: [{service(), options()}]. |
43 |
|
-type service_map() :: #{service() => options()}. |
44 |
|
|
45 |
|
-export_type([service/0, service_list/0, service_map/0, options/0]). |
46 |
|
|
47 |
|
-callback start(options()) -> start_result(). |
48 |
|
-callback stop() -> any(). |
49 |
|
-callback config_spec() -> mongoose_config_spec:config_section(). |
50 |
|
-callback deps() -> [service()]. |
51 |
|
|
52 |
|
-optional_callbacks([deps/0]). |
53 |
|
|
54 |
|
%% @doc Start all configured services in the dependency order. |
55 |
|
-spec start() -> ok. |
56 |
|
start() -> |
57 |
104 |
[start_service(Service, Opts) || {Service, Opts} <- sorted_services()], |
58 |
104 |
ok. |
59 |
|
|
60 |
|
%% @doc Stop all configured services in the reverse dependency order |
61 |
|
%% to avoid stopping services which have other services dependent on them. |
62 |
|
-spec stop() -> ok. |
63 |
|
stop() -> |
64 |
104 |
[stop_service(Service) || {Service, _Opts} <- lists:reverse(sorted_services())], |
65 |
104 |
ok. |
66 |
|
|
67 |
|
%% @doc Replace services at runtime - only for testing and debugging. |
68 |
|
%% Running services from ToStop are stopped and services from ToEnsure are (re)started when needed. |
69 |
|
%% Unused dependencies are stopped if no running services depend on them anymore. |
70 |
|
%% To prevent an unused dependency from being stopped, you need to include it in ToEnsure. |
71 |
|
-spec replace_services([service()], service_map()) -> ok. |
72 |
|
replace_services(ToStop, ToEnsure) -> |
73 |
4 |
Current = loaded_services_with_opts(), |
74 |
4 |
Old = maps:with(ToStop ++ maps:keys(ToEnsure), Current), |
75 |
4 |
OldWithDeps = mongoose_service_deps:resolve_deps(Old), |
76 |
4 |
SortedOldWithDeps = mongoose_service_deps:sort_deps(OldWithDeps), |
77 |
4 |
WithoutOld = maps:without(maps:keys(OldWithDeps), Current), |
78 |
4 |
WithNew = maps:merge(WithoutOld, ToEnsure), |
79 |
4 |
Target = mongoose_service_deps:resolve_deps(WithNew), |
80 |
|
|
81 |
|
%% Stop each affected service if it is not in Target (stop deps first) |
82 |
4 |
[ensure_stopped(Service) || {Service, _} <- lists:reverse(SortedOldWithDeps), |
83 |
5 |
not maps:is_key(Service, Target)], |
84 |
|
|
85 |
|
%% Ensure each service from Target |
86 |
4 |
[ensure_started(Service, Opts) || {Service, Opts} <- mongoose_service_deps:sort_deps(Target)], |
87 |
4 |
ok. |
88 |
|
|
89 |
|
-spec config_spec(service()) -> mongoose_config_spec:config_section(). |
90 |
|
config_spec(Service) -> |
91 |
208 |
Service:config_spec(). |
92 |
|
|
93 |
|
-spec get_deps(service()) -> [service()]. |
94 |
|
get_deps(Service) -> |
95 |
646 |
case mongoose_lib:is_exported(Service, deps, 0) of |
96 |
:-( |
true -> Service:deps(); |
97 |
646 |
false -> [] |
98 |
|
end. |
99 |
|
|
100 |
|
-spec sorted_services() -> service_list(). |
101 |
|
sorted_services() -> |
102 |
208 |
mongoose_service_deps:sort_deps(loaded_services_with_opts()). |
103 |
|
|
104 |
|
-spec set_services(service_map()) -> ok. |
105 |
|
set_services(Services) -> |
106 |
533 |
mongoose_config:set_opt(services, Services). |
107 |
|
|
108 |
|
-spec ensure_stopped(service()) -> {stopped, options()} | already_stopped. |
109 |
|
ensure_stopped(Service) -> |
110 |
288 |
case loaded_services_with_opts() of |
111 |
|
#{Service := Opts} = Services -> |
112 |
264 |
stop_service(Service, Services), |
113 |
264 |
{stopped, Opts}; |
114 |
|
_Services -> |
115 |
24 |
already_stopped |
116 |
|
end. |
117 |
|
|
118 |
|
-spec ensure_started(service(), options()) -> |
119 |
|
{started, start_result()} | {restarted, options(), start_result()} | already_started. |
120 |
|
ensure_started(Service, Opts) -> |
121 |
275 |
case loaded_services_with_opts() of |
122 |
|
#{Service := Opts} -> |
123 |
9 |
already_started; |
124 |
|
#{Service := PrevOpts} = Services -> |
125 |
3 |
stop_service(Service, Services), |
126 |
3 |
{ok, Result} = start_service(Service, Opts, Services), |
127 |
3 |
{restarted, PrevOpts, Result}; |
128 |
|
Services -> |
129 |
263 |
{ok, Result} = start_service(Service, Opts, Services), |
130 |
263 |
{started, Result} |
131 |
|
end. |
132 |
|
|
133 |
|
-spec start_service(service(), options(), service_map()) -> {ok, start_result()}. |
134 |
|
start_service(Service, Opts, Services) -> |
135 |
266 |
set_services(Services#{Service => Opts}), |
136 |
266 |
try |
137 |
266 |
start_service(Service, Opts) |
138 |
|
catch |
139 |
|
C:R:S -> |
140 |
:-( |
set_services(Services), |
141 |
:-( |
erlang:raise(C, R, S) |
142 |
|
end. |
143 |
|
|
144 |
|
-spec stop_service(service(), service_map()) -> ok. |
145 |
|
stop_service(Service, Services) -> |
146 |
267 |
stop_service(Service), |
147 |
267 |
set_services(maps:remove(Service, Services)). |
148 |
|
|
149 |
|
start_service(Service, Opts) -> |
150 |
474 |
assert_loaded(Service), |
151 |
474 |
try |
152 |
474 |
Res = Service:start(Opts), |
153 |
474 |
?LOG_INFO(#{what => service_started, service => Service, |
154 |
474 |
text => <<"Started MongooseIM service">>}), |
155 |
474 |
case Res of |
156 |
128 |
{ok, _} -> Res; |
157 |
346 |
_ -> {ok, Res} |
158 |
|
end |
159 |
|
catch Class:Reason:Stacktrace -> |
160 |
:-( |
?LOG_CRITICAL(#{what => service_startup_failed, |
161 |
|
text => <<"Failed to start MongooseIM service">>, |
162 |
|
service => Service, opts => Opts, |
163 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
164 |
:-( |
erlang:raise(Class, Reason, Stacktrace) |
165 |
|
end. |
166 |
|
|
167 |
|
stop_service(Service) -> |
168 |
471 |
assert_loaded(Service), |
169 |
471 |
try Service:stop() of |
170 |
|
_ -> |
171 |
471 |
?LOG_INFO(#{what => service_stopped, service => Service, |
172 |
471 |
text => <<"Stopped MongooseIM service">>}), |
173 |
471 |
ok |
174 |
|
catch Class:Reason:Stacktrace -> |
175 |
:-( |
?LOG_ERROR(#{what => service_stop_failed, service => Service, |
176 |
|
text => <<"Failed to stop MongooseIM service">>, |
177 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
178 |
:-( |
erlang:raise(Class, Reason, Stacktrace) |
179 |
|
end. |
180 |
|
|
181 |
|
-spec assert_loaded(service()) -> ok. |
182 |
|
assert_loaded(Service) -> |
183 |
945 |
case is_loaded(Service) of |
184 |
|
true -> |
185 |
945 |
ok; |
186 |
|
false -> |
187 |
:-( |
error(#{what => service_not_loaded, |
188 |
|
text => <<"Service missing from mongoose_config">>, |
189 |
|
service => Service}) |
190 |
|
end. |
191 |
|
|
192 |
|
-spec is_loaded(service()) -> boolean(). |
193 |
|
is_loaded(Service) -> |
194 |
1714 |
case mongoose_config:lookup_opt([services, Service]) of |
195 |
1445 |
{ok, _Opts} -> true; |
196 |
269 |
{error, not_found} -> false |
197 |
|
end. |
198 |
|
|
199 |
|
-spec loaded_services_with_opts() -> service_map(). |
200 |
|
loaded_services_with_opts() -> |
201 |
783 |
mongoose_config:get_opt(services). |