| % Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| % use this file except in compliance with the License. You may obtain a copy of |
| % the License at |
| % |
| % http://www.apache.org/licenses/LICENSE-2.0 |
| % |
| % Unless required by applicable law or agreed to in writing, software |
| % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| % License for the specific language governing permissions and limitations under |
| % the License. |
| |
| -module(couch_epi_sup). |
| |
| %% -------------------- |
| %% Important assumption |
| %% ==================== |
| %% Keeper and codechange_monitor childspecs rely on undocumented behaviour. |
| %% According to supervisor docs: |
| %% ...if the child process is a supervisor, gen_server, or gen_fsm, this |
| %% should be a list with one element [Module]. |
| %% However it is perfectly fine to have more than one module in the list. |
| %% Modules property is used to determine if process is suspendable. |
| %% Only suspendable processes are hot code upgraded, others are killed. |
| %% The check looks like `lists:member(Module, Modules)` |
| %% The assumption is that it is indeed underdocumented fact and not |
| %% an implementation detail. |
| |
| -behaviour(supervisor). |
| |
| -include("couch_epi.hrl"). |
| |
| %% API |
| -export([start_link/0]). |
| -export([plugin_childspecs/2]). |
| |
| %% Supervisor callbacks |
| -export([init/1]). |
| |
| %% Helper macro for declaring children of supervisor |
| -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). |
| |
| %% =================================================================== |
| %% API functions |
| %% =================================================================== |
| |
| start_link() -> |
| supervisor:start_link({local, ?MODULE}, ?MODULE, []). |
| |
| plugin_childspecs(Plugin, Children) -> |
| Plugins = application:get_env(couch_epi, plugins, []), |
| plugin_childspecs(Plugin, Plugins, Children). |
| |
| %% =================================================================== |
| %% Supervisor callbacks |
| %% =================================================================== |
| |
| init([]) -> |
| {ok, { {one_for_one, 5, 10}, keepers()} }. |
| |
| %% ------------------------------------------------------------------ |
| %% Internal Function Definitions |
| %% ------------------------------------------------------------------ |
| |
| keepers() -> |
| Plugins = application:get_env(couch_epi, plugins, []), |
| Definitions = couch_epi_plugin:grouped_definitions(Plugins), |
| Children = keeper_childspecs(Definitions), |
| remove_duplicates(Children). |
| |
| plugin_childspecs(Plugin, Plugins, Children) -> |
| Definitions = couch_epi_plugin:grouped_definitions([Plugin]), |
| ExtraChildren = couch_epi_plugin:plugin_processes(Plugin, Plugins), |
| merge(ExtraChildren, Children) ++ childspecs(Definitions). |
| |
| childspecs(Definitions) -> |
| lists:map(fun({{Kind, Key}, Defs}) -> |
| CodeGen = couch_epi_plugin:codegen(Kind), |
| Handle = CodeGen:get_handle(Key), |
| Modules = lists:append([modules(Spec) || {_App, Spec} <- Defs]), |
| Name = service_name(Key) ++ "|" ++ atom_to_list(Kind), |
| code_monitor(Name, [Handle], [Handle|Modules]) |
| end, Definitions). |
| |
| %% ------------------------------------------------------------------ |
| %% Helper Function Definitions |
| %% ------------------------------------------------------------------ |
| |
| remove_duplicates(Definitions) -> |
| lists:ukeysort(1, Definitions). |
| |
| keeper_childspecs(Definitions) -> |
| lists:map(fun({{Kind, Key}, _Specs}) -> |
| Name = service_name(Key) ++ "|keeper", |
| CodeGen = couch_epi_plugin:codegen(Kind), |
| Handle = CodeGen:get_handle(Key), |
| keeper(Name, [provider_kind(Kind), Key, CodeGen], [Handle]) |
| end, Definitions). |
| |
| keeper(Name, Args, Modules) -> |
| {"couch_epi|" ++ Name, {couch_epi_module_keeper, start_link, |
| Args}, permanent, 5000, worker, Modules}. |
| |
| code_monitor(Name, Args, Modules0) -> |
| Modules = [couch_epi_codechange_monitor | Modules0], |
| {"couch_epi_codechange_monitor|" ++ Name, |
| {couch_epi_codechange_monitor, start_link, Args}, permanent, 5000, worker, Modules}. |
| |
| provider_kind(services) -> providers; |
| provider_kind(data_subscriptions) -> data_providers; |
| provider_kind(Kind) -> Kind. |
| |
| service_name({ServiceId, Key}) -> |
| atom_to_list(ServiceId) ++ ":" ++ atom_to_list(Key); |
| service_name(ServiceId) -> |
| atom_to_list(ServiceId). |
| |
| modules(#couch_epi_spec{kind = providers, value = Module}) -> |
| [Module]; |
| modules(#couch_epi_spec{kind = services, value = Module}) -> |
| [Module]; |
| modules(#couch_epi_spec{kind = data_providers, value = Value}) -> |
| case Value of |
| {module, Module} -> [Module]; |
| _ -> [] |
| end; |
| modules(#couch_epi_spec{kind = data_subscriptions, behaviour = Module}) -> |
| [Module]. |
| |
| merge([], Children) -> |
| Children; |
| merge([{Id, _, _, _, _, _} = Spec | Rest], Children) -> |
| merge(Rest, lists:keystore(Id, 1, Children, Spec)). |
| |
| |
| %% ------------------------------------------------------------------ |
| %% Tests |
| %% ------------------------------------------------------------------ |
| |
| -ifdef(TEST). |
| -include_lib("eunit/include/eunit.hrl"). |
| |
| %% ---- |
| %% BEGIN couch_epi_plugin behaviour callbacks |
| |
| -compile([export_all]). |
| |
| app() -> test_app. |
| providers() -> |
| [ |
| {my_service, provider1}, |
| {my_service, provider2} |
| ]. |
| |
| services() -> |
| [ |
| {my_service, ?MODULE} |
| ]. |
| |
| data_providers() -> |
| [ |
| {{test_app, descriptions}, {module, ?MODULE}, [{interval, 100}]} |
| ]. |
| |
| data_subscriptions() -> |
| [ |
| {test_app, descriptions} |
| ]. |
| |
| processes() -> |
| [ |
| {?MODULE, [?CHILD(extra_process, worker)]}, |
| {?MODULE, [{to_replace, {new, start_link, [bar]}, |
| permanent, 5000, worker, [bar]}]} |
| ]. |
| |
| notify(_Key, _OldData, _NewData) -> |
| ok. |
| |
| %% END couch_epi_plugin behaviour callbacks |
| %% ---- |
| |
| parse_child_id(Id) when is_atom(Id) -> |
| Id; |
| parse_child_id(Id) -> |
| ["couch_epi_codechange_monitor", ServiceName, KindStr] = string:tokens(Id, "|"), |
| Kind = list_to_atom(KindStr), |
| case string:tokens(ServiceName, ":") of |
| [ServiceId, Key] -> |
| {{list_to_atom(ServiceId), list_to_atom(Key)}, Kind}; |
| [Key] -> |
| {list_to_atom(Key), Kind} |
| end. |
| |
| basic_test() -> |
| Expected = lists:sort([ |
| {extra_process, [], [extra_process]}, |
| {to_replace, [bar], [bar]}, |
| {{my_service, providers}, |
| [couch_epi_functions_gen_my_service], |
| [couch_epi_codechange_monitor, couch_epi_functions_gen_my_service, |
| provider1, provider2]}, |
| {{my_service, services}, |
| [couch_epi_functions_gen_my_service], |
| [couch_epi_codechange_monitor, couch_epi_functions_gen_my_service, |
| couch_epi_sup]}, |
| {{{test_app, descriptions}, data_subscriptions}, |
| [couch_epi_data_gen_test_app_descriptions], |
| [couch_epi_codechange_monitor, |
| couch_epi_data_gen_test_app_descriptions, couch_epi_sup]}, |
| {{{test_app, descriptions}, data_providers}, |
| [couch_epi_data_gen_test_app_descriptions], |
| [couch_epi_codechange_monitor, couch_epi_data_gen_test_app_descriptions, |
| couch_epi_sup]} |
| ]), |
| |
| ToReplace = {to_replace, {old, start_link, [foo]}, permanent, 5000, worker, [foo]}, |
| Children = lists:sort(plugin_childspecs(?MODULE, [?MODULE], [ToReplace])), |
| Results = [ |
| {parse_child_id(Id), Args, lists:sort(Modules)} |
| || {Id, {_M, _F, Args}, _, _, _, Modules} <- Children |
| ], |
| |
| Tests = lists:zip(Expected, Results), |
| [?assertEqual(Expect, Result) || {Expect, Result} <- Tests], |
| |
| ExpectedChild = {to_replace, {new, start_link, [bar]}, |
| permanent, 5000, worker, [bar]}, |
| ?assertEqual( |
| ExpectedChild, |
| lists:keyfind(to_replace, 1, Children)), |
| |
| ok. |
| |
| -endif. |