Refactor couch_epi to simplify it
diff --git a/src/couch_epi.erl b/src/couch_epi.erl
index 7388ac2..ab3703e 100644
--- a/src/couch_epi.erl
+++ b/src/couch_epi.erl
@@ -13,7 +13,7 @@
-module(couch_epi).
%% subscribtion management
--export([subscribe/5, unsubscribe/1, get_handle/1]).
+-export([get_handle/1]).
-export([register_service/1]).
%% queries and introspection
@@ -28,7 +28,18 @@
-export([is_configured/3]).
--export_type([service_id/0, app/0, key/0, handle/0, notify_cb/0]).
+%% ------------------------------------------------------------------
+%% Types Definitions
+%% ------------------------------------------------------------------
+
+-export_type([
+ service_id/0,
+ app/0,
+ key/0,
+ handle/0,
+ plugin_id/0,
+ data_spec/0
+]).
-type app() :: atom().
-type key() :: term().
@@ -36,11 +47,7 @@
-type properties() :: [{key(), term()}].
--type notification() :: {data, term()} | {modules, [module()]}.
--type notify_cb() :: fun(
- (App :: app(), Key :: key(), Data :: notification(), Extra :: term()) -> ok).
-
--type subscription() :: term().
+-type plugin_id() :: module().
-opaque handle() :: module().
@@ -51,6 +58,12 @@
-type apply_opts() :: [apply_opt()].
+-type data_spec()
+ :: {module, module()}
+ | {priv_file, FileName :: string()}
+ | {file, FileName :: string()}.
+
+
%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------
@@ -115,22 +128,6 @@
subscribers(Handle) ->
couch_epi_data_gen:subscribers(Handle).
-
-%% Passed MFA should implement notify_cb() type
--spec subscribe(App :: app(), Key :: key(),
- Module :: module(), Function :: atom(), Args :: [term()]) ->
- {ok, SubscriptionId :: subscription()}.
-
-subscribe(App, Key, M, F, A) ->
- couch_epi_server:subscribe(App, Key, {M, F, A}).
-
-
--spec unsubscribe(SubscriptionId :: subscription()) -> ok.
-
-unsubscribe(SubscriptionId) ->
- couch_epi_server:unsubscribe(SubscriptionId).
-
-%% The success typing is (atom() | tuple(),_,_,[any()],_) -> [any()]
-spec apply(Handle :: handle(), ServiceId :: atom(), Function :: atom(),
Args :: [term()], Opts :: apply_opts()) -> ok.
@@ -166,14 +163,8 @@
[] /= couch_epi_functions_gen:modules(Handle, Function, Arity).
--spec register_service({ServiceId :: service_id(), Key :: key()}) -> ok;
- (ServiceId :: service_id()) -> ok.
+-spec register_service(PluginId :: plugin_id()) ->
+ [supervisor:child_spec()].
-register_service({_ServiceId, _Key} = EPIKey) ->
- register_service(couch_epi_data_gen, EPIKey);
-register_service(ServiceId) when is_atom(ServiceId) ->
- register_service(couch_epi_functions_gen, ServiceId).
-
-register_service(Codegen, Key) ->
- Handle = Codegen:get_handle(Key),
- couch_epi_module_keeper:register_service(Codegen, Handle).
+register_service(Plugin) ->
+ couch_epi_sup:plugin_childspecs(Plugin).
diff --git a/src/couch_epi.hrl b/src/couch_epi.hrl
new file mode 100644
index 0000000..a8bd1d5
--- /dev/null
+++ b/src/couch_epi.hrl
@@ -0,0 +1,15 @@
+% 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.
+
+-record(couch_epi_spec, {
+ behaviour, app, kind, options, key, value, codegen, type
+}).
diff --git a/src/couch_epi_codechange_monitor.erl b/src/couch_epi_codechange_monitor.erl
new file mode 100644
index 0000000..7384804
--- /dev/null
+++ b/src/couch_epi_codechange_monitor.erl
@@ -0,0 +1,63 @@
+% 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_codechange_monitor).
+
+-behaviour(gen_server).
+
+%% ------------------------------------------------------------------
+%% API Function Exports
+%% ------------------------------------------------------------------
+
+-export([start_link/1]).
+
+%% ------------------------------------------------------------------
+%% gen_server Function Exports
+%% ------------------------------------------------------------------
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+%% ------------------------------------------------------------------
+%% API Function Definitions
+%% ------------------------------------------------------------------
+
+start_link(Handler) ->
+ gen_server:start_link(?MODULE, [Handler], []).
+
+%% ------------------------------------------------------------------
+%% gen_server Function Definitions
+%% ------------------------------------------------------------------
+
+init([Handler]) ->
+ couch_epi_module_keeper:reload(Handler),
+ {ok, Handler}.
+
+handle_call(_Request, _From, State) ->
+ {reply, ok, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, Keeper, _Extra) ->
+ couch_epi_module_keeper:reload(Keeper),
+ {ok, Keeper}.
+
+%% ------------------------------------------------------------------
+%% Internal Function Definitions
+%% ------------------------------------------------------------------
diff --git a/src/couch_epi_codegen.erl b/src/couch_epi_codegen.erl
index caedb88..1901887 100644
--- a/src/couch_epi_codegen.erl
+++ b/src/couch_epi_codegen.erl
@@ -50,7 +50,6 @@
case erl_parse:parse_form(Forms) of
{ok, AST} -> {ok, AST};
{error,{_,_, Reason}} ->
- %%Expr = [E || {E, _Form} <- Tokens],
{error, Expr, Reason}
end.
diff --git a/src/couch_epi_data.erl b/src/couch_epi_data.erl
index 502ed69..9370482 100644
--- a/src/couch_epi_data.erl
+++ b/src/couch_epi_data.erl
@@ -12,139 +12,103 @@
-module(couch_epi_data).
--behaviour(gen_server).
+-include("couch_epi.hrl").
%% ------------------------------------------------------------------
%% API Function Exports
%% ------------------------------------------------------------------
--export([childspec/4]).
--export([start_link/4, reload/1]).
--export([wait/1, stop/1]).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Exports
-%% ------------------------------------------------------------------
-
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
- terminate/2, code_change/3]).
-
--record(state, {
- subscriber, module, key, hash, handle,
- initialized = false, pending = []}).
+-export([interval/1, data/1]).
%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------
-childspec(Id, App, EpiKey, Module) ->
- {
- Id,
- {?MODULE, start_link, [
- App,
- EpiKey,
- Module,
- []
- ]},
- permanent,
- 5000,
- worker,
- [Module]
- }.
+interval(Specs) ->
+ extract_minimal_interval(Specs).
-start_link(SubscriberApp, {epi_key, Key}, Module, Options) ->
- maybe_start_keeper(Key),
- gen_server:start_link(?MODULE, [SubscriberApp, Module, Key, Options], []).
-
-reload(Server) ->
- gen_server:call(Server, reload).
-
-wait(Server) ->
- gen_server:call(Server, wait).
-
-stop(Server) ->
- catch gen_server:call(Server, stop).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Definitions
-%% ------------------------------------------------------------------
-
-init([Subscriber, Module, Key, _Options]) ->
- gen_server:cast(self(), init),
- {ok, #state{
- subscriber = Subscriber,
- module = Module,
- key = Key,
- handle = couch_epi_data_gen:get_handle(Key)}}.
-
-handle_call(wait, _From, #state{initialized = true} = State) ->
- {reply, ok, State};
-handle_call(wait, From, #state{pending = Pending} = State) ->
- {noreply, State#state{pending = [From | Pending]}};
-handle_call(reload, _From, State) ->
- {Res, NewState} = reload_if_updated(State),
- {reply, Res, NewState};
-handle_call(stop, _From, State) ->
- {stop, normal, State};
-handle_call(_Request, _From, State) ->
- {reply, ok, State}.
-
-handle_cast(init, #state{pending = Pending} = State) ->
- {_, NewState} = reload_if_updated(State),
- [gen_server:reply(Client, ok) || Client <- Pending],
- {noreply, NewState#state{initialized = true, pending = []}};
-handle_cast(_Msg, State) ->
- {noreply, State}.
-
-handle_info(_Info, State) ->
- {noreply, State}.
-
-terminate(_Reason, _State) ->
- ok.
-
-code_change(_OldVsn, State, _Extra) ->
- {_, NewState} = reload_if_updated(State),
- {ok, NewState}.
+data(Specs) ->
+ Locators = locate_sources(Specs),
+ case lists:foldl(fun collect_data/2, {ok, [], []}, Locators) of
+ {ok, Hashes, Data} ->
+ {ok, couch_epi_util:hash(Hashes), Data};
+ Error ->
+ Error
+ end.
%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------
-reload_if_updated(#state{handle = Module} = State) ->
- case couch_epi_util:module_exists(Module) of
+collect_data({App, Locator}, {ok, HashAcc, DataAcc}) ->
+ case definitions(Locator) of
+ {ok, Hash, Data} ->
+ {ok, [Hash | HashAcc], [{App, Data} | DataAcc]};
+ Error ->
+ Error
+ end;
+collect_data({_App, _Locator}, Error) ->
+ Error.
+
+extract_minimal_interval(Specs) ->
+ lists:foldl(fun minimal_interval/2, undefined, Specs).
+
+minimal_interval({_App, #couch_epi_spec{options = Options}}, Min) ->
+ case lists:keyfind(interval, 1, Options) of
+ {interval, Interval} -> min(Interval, Min);
+ false -> Min
+ end.
+
+locate_sources(Specs) ->
+ lists:map(fun({ProviderApp, #couch_epi_spec{value = Src}}) ->
+ {ok, Locator} = locate(ProviderApp, Src),
+ {ProviderApp, Locator}
+ end, Specs).
+
+locate(App, {priv_file, FileName}) ->
+ case priv_path(App, FileName) of
+ {ok, FilePath} ->
+ ok = check_exists(FilePath),
+ {ok, {file, FilePath}};
+ Else ->
+ Else
+ end;
+locate(_App, {file, FilePath}) ->
+ ok = check_exists(FilePath),
+ {ok, {file, FilePath}};
+locate(_App, Locator) ->
+ {ok, Locator}.
+
+priv_path(AppName, FileName) ->
+ case code:priv_dir(AppName) of
+ {error, _Error} = Error ->
+ Error;
+ Dir ->
+ {ok, filename:join(Dir, FileName)}
+ end.
+
+check_exists(FilePath) ->
+ case filelib:is_regular(FilePath) of
true ->
- do_reload_if_updated(State);
+ ok;
false ->
- {ok, State}
+ {error, {notfound, FilePath}}
end.
-do_reload_if_updated(#state{hash = OldHash, module = Module} = State) ->
- case couch_epi_functions_gen:hash([Module]) of
- OldHash ->
- {ok, State};
- Hash ->
- safe_set(Hash, State)
- end.
+definitions({file, FilePath}) ->
+ case file:consult(FilePath) of
+ {ok, Data} ->
+ {ok, hash_of_file(FilePath), Data};
+ {error, Reason} ->
+ {error, {FilePath, Reason}}
+ end;
+definitions({module, Module}) when is_atom(Module) ->
+ definitions({module, [Module]});
+definitions({module, Modules}) ->
+ Data = lists:append([M:data() || M <- Modules]),
+ Hash = couch_epi_functions_gen:hash(Modules),
+ {ok, Hash, Data}.
-safe_set(Hash, #state{} = State) ->
- #state{
- handle = Handle,
- subscriber = Subscriber,
- module = Module,
- key = Key} = State,
- try
- Data = get_from_module(Module),
- OldData = couch_epi_data_gen:current_data(Handle, Subscriber),
- ok = couch_epi_data_gen:set(Handle, Subscriber, Data),
- couch_epi_server:notify(Subscriber, Key, {data, OldData}, {data, Data}),
- {ok, State#state{hash = Hash}}
- catch Class:Reason ->
- {{Class, Reason}, State}
- end.
-
-get_from_module(Module) ->
- Module:data().
-
-maybe_start_keeper(Key) ->
- Handle = couch_epi_data_gen:get_handle(Key),
- couch_epi_module_keeper:maybe_start_keeper(couch_epi_data_gen, Handle).
+hash_of_file(FilePath) ->
+ {ok, Data} = file:read_file(FilePath),
+ couch_epi_util:md5(Data).
diff --git a/src/couch_epi_data_gen.erl b/src/couch_epi_data_gen.erl
index d7e0c65..16a5986 100644
--- a/src/couch_epi_data_gen.erl
+++ b/src/couch_epi_data_gen.erl
@@ -18,21 +18,13 @@
%% To get an idea about he code of the generated module see preamble()
-export([get_handle/1]).
--export([set/3, get/1, get/2, get/3]).
+-export([get/1, get/2, get/3]).
+-export([generate/2]).
-export([by_key/1, by_key/2]).
-export([by_source/1, by_source/2]).
-export([keys/1, subscribers/1]).
--export([save/3]).
--export([current_data/2]).
-
-set(Handle, Source, Data) ->
- case is_updated(Handle, Source, Data) of
- false ->
- ok;
- true ->
- couch_epi_module_keeper:save(Handle, Source, Data)
- end.
+-export([get_current_definitions/1]).
get(Handle) ->
Handle:all().
@@ -173,36 +165,8 @@
module_name({Service, Key}) when is_list(Service) andalso is_list(Key) ->
list_to_atom(string:join([atom_to_list(?MODULE), Service, Key], "_")).
-is_updated(Handle, Source, Data) ->
- Sig = couch_epi_util:hash(Data),
- if_exists(Handle, version, 1, true, fun() ->
- try Handle:version(Source) of
- {error, {unknown, Source}} -> true;
- {error, Reason} -> throw(Reason);
- Sig -> false;
- _ -> true
- catch
- Class:Reason ->
- throw({Class, {Source, Reason}})
- end
- end).
-save(Handle, undefined, []) ->
- case current_data(Handle) of
- [] -> generate(Handle, []);
- _Else -> ok
- end;
-save(Handle, Source, Data) ->
- CurrentData = current_data(Handle),
- NewDefs = lists:keystore(Source, 1, CurrentData, {Source, Data}),
- generate(Handle, NewDefs).
-
-current_data(Handle, Subscriber) ->
- if_exists(Handle, by_source, 1, [], fun() ->
- Handle:by_source(Subscriber)
- end).
-
-current_data(Handle) ->
+get_current_definitions(Handle) ->
if_exists(Handle, by_source, 0, [], fun() ->
Handle:by_source()
end).
@@ -237,76 +201,66 @@
-include_lib("eunit/include/eunit.hrl").
basic_test() ->
- try
- Module = foo_bar_baz_bugz,
+ Module = foo_bar_baz_bugz,
- meck:new(couch_epi_module_keeper, [passthrough]),
- meck:expect(couch_epi_module_keeper, save, fun
- (Handle, Source, Modules) -> save(Handle, Source, Modules)
- end),
+ Data1 = [some_nice_data],
+ Data2 = "other data",
+ Data3 = {"even more data"},
+ Defs1 = [{foo, Data1}],
+ Defs2 = lists:usort([{foo, Data2}, {bar, Data3}]),
- Data1 = [some_nice_data],
- Data2 = "other data",
- Data3 = {"even more data"},
- Defs1 = [{foo, Data1}],
- Defs2 = lists:usort([{foo, Data2}, {bar, Data3}]),
+ Defs = [{app1, Defs1}, {app2, Defs2}],
+ generate(Module, Defs),
- set(Module, app1, Defs1),
- set(Module, app2, Defs2),
+ ?assertEqual([bar, foo], lists:usort(Module:keys())),
+ ?assertEqual([app1, app2], lists:usort(Module:subscribers())),
- ?assertEqual([bar, foo], lists:usort(Module:keys())),
- ?assertEqual([app1, app2], lists:usort(Module:subscribers())),
+ ?assertEqual(Data1, Module:get(app1, foo)),
+ ?assertEqual(Data2, Module:get(app2, foo)),
+ ?assertEqual(Data3, Module:get(app2, bar)),
- ?assertEqual(Data1, Module:get(app1, foo)),
- ?assertEqual(Data2, Module:get(app2, foo)),
- ?assertEqual(Data3, Module:get(app2, bar)),
+ ?assertEqual(undefined, Module:get(bad, key)),
+ ?assertEqual(undefined, Module:get(source, bad)),
- ?assertEqual(undefined, Module:get(bad, key)),
- ?assertEqual(undefined, Module:get(source, bad)),
+ ?assertEqual("3KZ4EG4WBF4J683W8GSDDPYR3", Module:version(app1)),
+ ?assertEqual("4EFUU47W9XDNMV9RMZSSJQU3Y", Module:version(app2)),
- ?assertEqual("3KZ4EG4WBF4J683W8GSDDPYR3", Module:version(app1)),
- ?assertEqual("4EFUU47W9XDNMV9RMZSSJQU3Y", Module:version(app2)),
+ ?assertEqual({error,{unknown,bad}}, Module:version(bad)),
- ?assertEqual({error,{unknown,bad}}, Module:version(bad)),
+ ?assertEqual(
+ [{app1,"3KZ4EG4WBF4J683W8GSDDPYR3"},
+ {app2,"4EFUU47W9XDNMV9RMZSSJQU3Y"}], lists:usort(Module:version())),
- ?assertEqual(
- [{app1,"3KZ4EG4WBF4J683W8GSDDPYR3"},
- {app2,"4EFUU47W9XDNMV9RMZSSJQU3Y"}], lists:usort(Module:version())),
+ ?assertEqual(
+ [{app1,[some_nice_data]},{app2,"other data"}],
+ lists:usort(Module:by_key(foo))),
- ?assertEqual(
- [{app1,[some_nice_data]},{app2,"other data"}],
- lists:usort(Module:by_key(foo))),
+ ?assertEqual([], lists:usort(Module:by_key(bad))),
- ?assertEqual([], lists:usort(Module:by_key(bad))),
-
- ?assertEqual(
- [
- {bar, [{app2, {"even more data"}}]},
- {foo, [{app2, "other data"}, {app1, [some_nice_data]}]}
- ],
- lists:usort(Module:by_key())),
+ ?assertEqual(
+ [
+ {bar, [{app2, {"even more data"}}]},
+ {foo, [{app2, "other data"}, {app1, [some_nice_data]}]}
+ ],
+ lists:usort(Module:by_key())),
- ?assertEqual(Defs1, lists:usort(Module:by_source(app1))),
- ?assertEqual(Defs2, lists:usort(Module:by_source(app2))),
+ ?assertEqual(Defs1, lists:usort(Module:by_source(app1))),
+ ?assertEqual(Defs2, lists:usort(Module:by_source(app2))),
- ?assertEqual([], lists:usort(Module:by_source(bad))),
+ ?assertEqual([], lists:usort(Module:by_source(bad))),
- ?assertEqual(
- [
- {app1, [{foo, [some_nice_data]}]},
- {app2, [{foo, "other data"}, {bar, {"even more data"}}]}
- ],
- lists:usort(Module:by_source())),
+ ?assertEqual(
+ [
+ {app1, [{foo, [some_nice_data]}]},
+ {app2, [{foo, "other data"}, {bar, {"even more data"}}]}
+ ],
+ lists:usort(Module:by_source())),
- ?assertEqual(
- lists:usort([Data1, Data2, Data3]), lists:usort(Module:all())),
- ?assertEqual(lists:usort([Data1, Data2]), lists:usort(Module:all(foo))),
- ?assertEqual([], lists:usort(Module:all(bad))),
- ok
- after
- meck:unload(couch_epi_module_keeper)
- end,
+ ?assertEqual(
+ lists:usort([Data1, Data2, Data3]), lists:usort(Module:all())),
+ ?assertEqual(lists:usort([Data1, Data2]), lists:usort(Module:all(foo))),
+ ?assertEqual([], lists:usort(Module:all(bad))),
ok.
-endif.
diff --git a/src/couch_epi_data_source.erl b/src/couch_epi_data_source.erl
deleted file mode 100644
index ef83b14..0000000
--- a/src/couch_epi_data_source.erl
+++ /dev/null
@@ -1,194 +0,0 @@
-% 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_data_source).
-
--behaviour(gen_server).
--define(MONITOR_INTERVAL, 5000).
-
-%% ------------------------------------------------------------------
-%% API Function Exports
-%% ------------------------------------------------------------------
-
--export([childspec/5]).
--export([start_link/4, reload/1]).
--export([wait/1, stop/1]).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Exports
-%% ------------------------------------------------------------------
-
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
- terminate/2, code_change/3]).
-
--record(state, {
- subscriber, locator, key, hash, handle,
- initialized = false, pending = []}).
-
-%% ------------------------------------------------------------------
-%% API Function Definitions
-%% ------------------------------------------------------------------
-
-childspec(Id, App, EpiKey, Locator, Options) ->
- {
- Id,
- {?MODULE, start_link, [
- App,
- EpiKey,
- Locator,
- Options
- ]},
- permanent,
- 5000,
- worker,
- [?MODULE]
- }.
-
-start_link(SubscriberApp, {epi_key, Key}, Src, Options) ->
- maybe_start_keeper(Key),
- {ok, Locator} = locate(SubscriberApp, Src),
- gen_server:start_link(?MODULE, [SubscriberApp, Locator, Key, Options], []).
-
-reload(Server) ->
- gen_server:call(Server, reload).
-
-wait(Server) ->
- gen_server:call(Server, wait).
-
-stop(Server) ->
- catch gen_server:call(Server, stop).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Definitions
-%% ------------------------------------------------------------------
-
-init([Subscriber, Locator, Key, Options]) ->
- gen_server:cast(self(), init),
- Interval = proplists:get_value(interval, Options, ?MONITOR_INTERVAL),
- {ok, _Timer} = timer:send_interval(Interval, self(), tick),
- {ok, #state{
- subscriber = Subscriber,
- locator = Locator,
- key = Key,
- handle = couch_epi_data_gen:get_handle(Key)}}.
-
-handle_call(wait, _From, #state{initialized = true} = State) ->
- {reply, ok, State};
-handle_call(wait, From, #state{pending = Pending} = State) ->
- {noreply, State#state{pending = [From | Pending]}};
-handle_call(reload, _From, State) ->
- {Res, NewState} = reload_if_updated(State),
- {reply, Res, NewState};
-handle_call(stop, _From, State) ->
- {stop, normal, State};
-handle_call(_Request, _From, State) ->
- {reply, ok, State}.
-
-handle_cast(init, #state{pending = Pending} = State) ->
- {_, NewState} = reload_if_updated(State),
- [gen_server:reply(Client, ok) || Client <- Pending],
- {noreply, NewState#state{initialized = true, pending = []}};
-handle_cast(_Msg, State) ->
- {noreply, State}.
-
-handle_info(tick, State0) ->
- {_Res, State1} = reload_if_updated(State0),
- {noreply, State1};
-handle_info(_Info, State) ->
- {noreply, State}.
-
-terminate(_Reason, _State) ->
- ok.
-
-code_change(_OldVsn, State, _Extra) ->
- {ok, State}.
-
-%% ------------------------------------------------------------------
-%% Internal Function Definitions
-%% ------------------------------------------------------------------
-
-locate(App, {priv_file, FileName}) ->
- case priv_path(App, FileName) of
- {ok, FilePath} ->
- ok = ensure_exists(FilePath),
- {ok, {file, FilePath}};
- Else ->
- Else
- end;
-locate(_App, {file, FilePath}) ->
- ok = ensure_exists(FilePath),
- {ok, {file, FilePath}}.
-
-priv_path(AppName, FileName) ->
- case code:priv_dir(AppName) of
- {error, _Error} = Error ->
- Error;
- Dir ->
- {ok, filename:join(Dir, FileName)}
- end.
-
-ensure_exists(FilePath) ->
- case filelib:is_regular(FilePath) of
- true ->
- ok;
- false ->
- {error, {notfound, FilePath}}
- end.
-
-reload_if_updated(#state{handle = Module} = State) ->
- case couch_epi_util:module_exists(Module) of
- true ->
- do_reload_if_updated(State);
- false ->
- {ok, State}
- end.
-
-do_reload_if_updated(#state{hash = OldHash, locator = Locator} = State) ->
- case read(Locator) of
- {ok, OldHash, _Data} ->
- {ok, State};
- {ok, Hash, Data} ->
- safe_set(Hash, Data, State);
- Else ->
- {Else, State}
- end.
-
-safe_set(Hash, Data, #state{} = State) ->
- #state{
- handle = Handle,
- subscriber = Subscriber,
- key = Key} = State,
-
- try
- OldData = couch_epi_data_gen:current_data(Handle, Subscriber),
- ok = couch_epi_data_gen:set(Handle, Subscriber, Data),
- couch_epi_server:notify(Subscriber, Key, {data, OldData}, {data, Data}),
- {ok, State#state{hash = Hash}}
- catch Class:Reason ->
- {{Class, Reason}, State}
- end.
-
-read({file, FilePath}) ->
- case file:consult(FilePath) of
- {ok, Data} ->
- {ok, hash_of_file(FilePath), Data};
- {error, Reason} ->
- {error, {FilePath, Reason}}
- end.
-
-hash_of_file(FilePath) ->
- {ok, Data} = file:read_file(FilePath),
- couch_epi_util:md5(Data).
-
-maybe_start_keeper(Key) ->
- Handle = couch_epi_data_gen:get_handle(Key),
- couch_epi_module_keeper:maybe_start_keeper(couch_epi_data_gen, Handle).
diff --git a/src/couch_epi_functions.erl b/src/couch_epi_functions.erl
index 33c4f91..34d1a06 100644
--- a/src/couch_epi_functions.erl
+++ b/src/couch_epi_functions.erl
@@ -12,152 +12,37 @@
-module(couch_epi_functions).
--behaviour(gen_server).
+-include("couch_epi.hrl").
%% ------------------------------------------------------------------
%% API Function Exports
%% ------------------------------------------------------------------
--export([childspec/4]).
--export([start_link/4, reload/1]).
--export([wait/1, stop/1]).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Exports
-%% ------------------------------------------------------------------
-
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
- terminate/2, code_change/3]).
-
--record(state, {
- provider, service_id, modules, hash, handle,
- initialized = false, pending = []}).
+-export([interval/1, data/1]).
%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------
-childspec(Id, App, Key, Module) ->
- {
- Id,
- {?MODULE, start_link, [
- App,
- {epi_key, Key},
- {modules, [Module]},
- []
- ]},
- permanent,
- 5000,
- worker,
- [Module]
- }.
+interval(_) ->
+ undefined.
-start_link(ProviderApp, {epi_key, ServiceId}, {modules, Modules}, Options) ->
- maybe_start_keeper(ServiceId),
- gen_server:start_link(
- ?MODULE, [ProviderApp, ServiceId, Modules, Options], []).
-
-reload(Server) ->
- gen_server:call(Server, reload).
-
-wait(Server) ->
- gen_server:call(Server, wait).
-
-stop(Server) ->
- catch gen_server:call(Server, stop).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Definitions
-%% ------------------------------------------------------------------
-
-init([Provider, ServiceId, Modules, _Options]) ->
- gen_server:cast(self(), init),
- {ok, #state{
- provider = Provider,
- modules = Modules,
- service_id = ServiceId,
- handle = couch_epi_functions_gen:get_handle(ServiceId)}}.
-
-handle_call(wait, _From, #state{initialized = true} = State) ->
- {reply, ok, State};
-handle_call(wait, From, #state{pending = Pending} = State) ->
- {noreply, State#state{pending = [From | Pending]}};
-handle_call(reload, _From, State) ->
- {Res, NewState} = reload_if_updated(State),
- {reply, Res, NewState};
-handle_call(stop, _From, State) ->
- {stop, normal, State};
-handle_call(_Request, _From, State) ->
- {reply, ok, State}.
-
-handle_cast(init, #state{pending = Pending} = State) ->
- {_, NewState} = reload_if_updated(State),
- [gen_server:reply(Client, ok) || Client <- Pending],
- {noreply, NewState#state{initialized = true, pending = []}};
-handle_cast(_Msg, State) ->
- {noreply, State}.
-
-handle_info(_Info, State) ->
- {noreply, State}.
-
-terminate(_Reason, State) ->
- safe_remove(State),
- ok.
-
-code_change(_OldVsn, State, _Extra) ->
- {_, NewState} = reload_if_updated(State),
- {ok, NewState}.
+data(Specs) ->
+ Defs = [{A, definitions(M)} || {A, #couch_epi_spec{value = M}} <- Specs],
+ Modules = lists:flatten([M || {_App, #couch_epi_spec{value = M}} <- Specs]),
+ {ok, couch_epi_functions_gen:hash(Modules), group(Defs)}.
%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------
-reload_if_updated(#state{handle = Module} = State) ->
- case couch_epi_util:module_exists(Module) of
- true ->
- do_reload_if_updated(State);
- false ->
- {ok, State}
- end.
+definitions(Module) when is_atom(Module) ->
+ definitions([Module]);
+definitions(Modules) ->
+ Blacklist = [{module_info, 0}, {module_info, 1}],
+ [{M, M:module_info(exports) -- Blacklist} || M <- Modules].
-do_reload_if_updated(#state{hash = OldHash, modules = Modules} = State) ->
- case couch_epi_functions_gen:hash(Modules) of
- OldHash ->
- {ok, State};
- Hash ->
- safe_add(Hash, State)
- end.
-
-safe_add(Hash, #state{modules = OldModules} = State) ->
- #state{
- handle = Handle,
- provider = Provider,
- modules = Modules,
- service_id = ServiceId} = State,
- try
- ok = couch_epi_functions_gen:add(Handle, Provider, Modules),
- couch_epi_server:notify(
- Provider, ServiceId, {modules, OldModules}, {modules, Modules}),
- {ok, State#state{hash = Hash}}
- catch Class:Reason ->
- {{Class, Reason}, State}
- end.
-
-safe_remove(#state{} = State) ->
- #state{
- handle = Handle,
- provider = Provider,
- modules = Modules,
- service_id = ServiceId} = State,
- try
- ok = couch_epi_functions_gen:remove(Handle, Provider, Modules),
- couch_epi_server:notify(
- Provider, ServiceId, {modules, Modules}, {modules, []}),
- {ok, State#state{modules = []}}
- catch Class:Reason ->
- {{Class, Reason}, State}
- end.
-
-maybe_start_keeper(ServiceId) ->
- Handle = couch_epi_functions_gen:get_handle(ServiceId),
- couch_epi_module_keeper:maybe_start_keeper(couch_epi_functions_gen, Handle).
+group(KV) ->
+ dict:to_list(lists:foldr(fun({K,V}, D) ->
+ dict:append_list(K, V, D)
+ end, dict:new(), KV)).
diff --git a/src/couch_epi_functions_gen.erl b/src/couch_epi_functions_gen.erl
index be04644..4990b60 100644
--- a/src/couch_epi_functions_gen.erl
+++ b/src/couch_epi_functions_gen.erl
@@ -12,9 +12,18 @@
-module(couch_epi_functions_gen).
--export([add/3, remove/3, get_handle/1, hash/1, apply/4, apply/5, modules/3]).
+-export([
+ generate/2,
+ get_current_definitions/1,
+ get_handle/1,
+ hash/1
+]).
--export([save/3]).
+-export([
+ apply/4,
+ apply/5,
+ modules/3
+]).
-ifdef(TEST).
@@ -24,29 +33,10 @@
-record(opts, {
ignore_errors = false,
- ignore_providers = false,
pipe = false,
concurrent = false
}).
-add(Handle, Source, Modules) ->
- case is_updated(Handle, Source, Modules) of
- false ->
- ok;
- true ->
- couch_epi_module_keeper:save(Handle, Source, Modules)
- end.
-
-remove(Handle, Source, Modules) ->
- CurrentDefs = get_current_definitions(Handle),
- {SourceDefs, Defs} = remove_from_definitions(CurrentDefs, Source),
-
- NewSourceDefs = lists:filter(fun({M, _}) ->
- not lists:member(M, Modules)
- end, SourceDefs),
-
- generate(Handle, Defs ++ NewSourceDefs).
-
get_handle(ServiceId) ->
module_name(atom_to_list(ServiceId)).
@@ -61,7 +51,6 @@
Modules = providers(Handle, Function, length(Args), DispatchOpts),
dispatch(Handle, Modules, Function, Args, DispatchOpts).
-
%% ------------------------------------------------------------------
%% Codegeneration routines
%% ------------------------------------------------------------------
@@ -188,36 +177,6 @@
module_name(ServiceId) when is_list(ServiceId) ->
list_to_atom(string:join([atom_to_list(?MODULE), ServiceId], "_")).
-is_updated(Handle, Source, Modules) ->
- Sig = hash(Modules),
- if_exists(Handle, version, 1, true, fun() ->
- try Handle:version(Source) of
- {error, {unknown, Source}} -> true;
- {error, Reason} -> throw(Reason);
- Sig -> false;
- _ -> true
- catch
- Class:Reason ->
- throw({Class, {Source, Reason}})
- end
- end).
-
-save(Handle, undefined, []) ->
- case get_current_definitions(Handle) of
- [] -> generate(Handle, []);
- _Else -> ok
- end;
-save(Handle, Source, Modules) ->
- CurrentDefs = get_current_definitions(Handle),
- Definitions = definitions(Source, Modules),
- NewDefs = lists:keystore(Source, 1, CurrentDefs, Definitions),
- generate(Handle, NewDefs).
-
-definitions(Source, Modules) ->
- Blacklist = [{module_info, 0}, {module_info, 1}],
- SrcDefs = [{M, M:module_info(exports) -- Blacklist} || M <- Modules],
- {Source, SrcDefs}.
-
get_current_definitions(Handle) ->
if_exists(Handle, definitions, 0, [], fun() ->
Handle:definitions()
@@ -249,10 +208,18 @@
end
),
Dict = lists:foldl(fun({K, V}, Acc) ->
- dict:append(K, V, Acc)
+ dict:update(K, fun(Modules) ->
+ append_if_missing(Modules, V)
+ end, [V], Acc)
+
end, dict:new(), Providers),
dict:to_list(Dict).
+append_if_missing(List, Value) ->
+ case lists:member(Value, List) of
+ true -> List;
+ false -> [Value | List]
+ end.
hash(Modules) ->
VSNs = [couch_epi_util:module_version(M) || M <- lists:usort(Modules)],
@@ -307,18 +274,10 @@
providers(Handle, Function, Arity, #opts{}) ->
Handle:providers(Function, Arity).
-remove_from_definitions(Defs, Source) ->
- case lists:keytake(Source, 1, Defs) of
- {value, {Source, Value}, Rest} ->
- {Value, Rest};
- false ->
- {[], Defs}
- end.
-
-spec modules(Handle :: atom(), Function :: atom(), Arity :: pos_integer()) ->
list().
modules(Handle, Function, Arity) ->
- providers(Handle, Function, Arity, #opts{ignore_providers = true}).
+ providers(Handle, Function, Arity, #opts{}).
%% ------------------------------------------------------------------
%% Tests
@@ -334,36 +293,36 @@
[].
basic_test() ->
- try
- Module = foo_bar_dispatcher,
- meck:new(couch_epi_module_keeper, [passthrough]),
- meck:expect(couch_epi_module_keeper, save, fun
- (Handle, Source, Modules) -> save(Handle, Source, Modules)
- end),
+ Module = foo_bar_dispatcher,
+ Defs = [{?MODULE, [{foo, 2}, {bar, 0}]}],
- add(Module, app1, [?MODULE]),
+ generate(Module, [{app1, Defs}, {app2, Defs}]),
- ?assertMatch([?MODULE], modules(Module, foo, 2)),
+ Exports = lists:sort([
+ {callbacks,2},
+ {version,1},
+ {providers,2},
+ {definitions,1},
+ {module_info,0},
+ {version,0},
+ {dispatch,3},
+ {providers,0},
+ {module_info,1},
+ {definitions,0}]),
- ?assert(is_list(Module:version(app1))),
+ ?assertEqual(Exports, lists:sort(Module:module_info(exports))),
+ ?assertEqual([app1, app2], lists:sort(Module:providers())),
- Defs1 = lists:usort(Module:definitions()),
- ?assertMatch([{app1, [{?MODULE, _}]}], Defs1),
- [{app1, [{?MODULE, Exports}]}] = Defs1,
- ?assert(lists:member({bar, 0}, Exports)),
+ ?assertEqual([?MODULE], lists:sort(Module:providers(foo, 2))),
+ ?assertEqual([?MODULE], lists:sort(Module:providers(bar, 0))),
- add(Module, app2, [?MODULE]),
- Defs2 = lists:usort(Module:definitions()),
- ?assertMatch([{app1, [{?MODULE, _}]}, {app2, [{?MODULE, _}]}], Defs2),
+ Defs2 = lists:usort(Module:definitions()),
+ ?assertMatch([{app1, [{?MODULE, _}]}, {app2, [{?MODULE, _}]}], Defs2),
- ?assertMatch([{app1, Hash}, {app2, Hash}], Module:version()),
+ ?assertMatch([{app1, Hash}, {app2, Hash}], Module:version()),
- ?assertMatch([], Module:dispatch(?MODULE, bar, [])),
- ?assertMatch({1, 2}, Module:dispatch(?MODULE, foo, [1, 2])),
- ok
- after
- meck:unload(couch_epi_module_keeper)
- end,
+ ?assertMatch([], Module:dispatch(?MODULE, bar, [])),
+ ?assertMatch({1, 2}, Module:dispatch(?MODULE, foo, [1, 2])),
ok.
diff --git a/src/couch_epi_keeper_sup.erl b/src/couch_epi_keeper_sup.erl
deleted file mode 100644
index c342d68..0000000
--- a/src/couch_epi_keeper_sup.erl
+++ /dev/null
@@ -1,58 +0,0 @@
-% 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_keeper_sup).
-
--behaviour(supervisor).
-
-%% ------------------------------------------------------------------
-%% API Function Exports
-%% ------------------------------------------------------------------
-
--export([start_link/0]).
-
--export([start_child/2, terminate_child/1]).
-
-%% ------------------------------------------------------------------
-%% supervisor Function Exports
-%% ------------------------------------------------------------------
-
--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, []).
-
-start_child(Codegen, Module) ->
- supervisor:start_child(?MODULE, [Codegen, Module]).
-
-terminate_child(undefined) -> ok;
-terminate_child(Child) when is_atom(Child) ->
- terminate_child(whereis(Child));
-terminate_child(ChildPid) ->
- supervisor:terminate_child(?MODULE, ChildPid).
-
-%% ===================================================================
-%% Supervisor callbacks
-%% ===================================================================
-
-init([]) ->
- Children = [
- ?CHILD(couch_epi_module_keeper, worker)
- ],
- {ok, { {simple_one_for_one, 5, 10}, Children} }.
diff --git a/src/couch_epi_module_keeper.erl b/src/couch_epi_module_keeper.erl
index 2d8c439..36376fe 100644
--- a/src/couch_epi_module_keeper.erl
+++ b/src/couch_epi_module_keeper.erl
@@ -12,17 +12,16 @@
-module(couch_epi_module_keeper).
+
-behaviour(gen_server).
%% ------------------------------------------------------------------
%% API Function Exports
%% ------------------------------------------------------------------
--export([maybe_start_keeper/2]).
--export([register_service/2]).
+-export([start_link/3, stop/1]).
+-export([reload/1]).
--export([start_link/2, save/3]).
--export([stop/1]).
%% ------------------------------------------------------------------
%% gen_server Function Exports
@@ -31,62 +30,132 @@
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
--record(state, {codegen, module}).
+-record(state, {
+ codegen, module, key, type, handle, hash, kind,
+ timer = {undefined, undefined}}).
%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------
-register_service(Codegen, Module) ->
- {ok, Server} = maybe_start_keeper(Codegen, Module),
- compile_dummy_module(Server).
-
-maybe_start_keeper(Codegen, Module) ->
- case couch_epi_keeper_sup:start_child(Codegen, Module) of
- {ok, Pid} ->
- {ok, Pid};
- {error, {already_started, Pid}} ->
- {ok, Pid}
- end.
-
-start_link(Codegen, Module) ->
- gen_server:start_link({local, Module}, ?MODULE, [Codegen, Module], []).
+start_link(Type, Key, Codegen) ->
+ Handle = Codegen:get_handle(Key),
+ gen_server:start_link(
+ {local, Handle}, ?MODULE, [Type, Codegen, Key, Handle], []).
stop(Server) ->
catch gen_server:call(Server, stop).
-save(Server, Source, Config) ->
- gen_server:call(Server, {save, Source, Config}).
+reload(Server) ->
+ gen_server:call(Server, reload).
%% ------------------------------------------------------------------
%% gen_server Function Definitions
%% ------------------------------------------------------------------
-init([Codegen, Module]) ->
- {ok, #state{codegen = Codegen, module = Module}}.
+init([Kind, Codegen, Key, Handle]) ->
+ Type = type(Kind),
+ State = #state{
+ codegen = Codegen,
+ key = Key,
+ type = Type,
+ handle = Handle,
+ kind = Kind
+ },
+ compile_module(State).
-handle_call({save, Source, Config}, _From, State) ->
- #state{codegen = Codegen, module = Module} = State,
- Reply = Codegen:save(Module, Source, Config),
- {reply, Reply, State};
+handle_call(reload, _From, State0) ->
+ {Reply, State1} = reload_if_updated(State0),
+ {reply, Reply, State1};
handle_call(_Request, _From, State) ->
{reply, ok, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
+handle_info(tick, State0) ->
+ {_Res, State1} = reload_if_updated(State0),
+ {noreply, State1};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
-code_change(_OldVsn, State, _Extra) ->
- {ok, State}.
+code_change(_OldVsn, State0, _Extra) ->
+ {_Res, State1} = reload_if_updated(State0),
+ {ok, State1}.
%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------
-compile_dummy_module(Server) ->
- save(Server, undefined, []).
+type(data_providers) -> couch_epi_data;
+type(providers) -> couch_epi_functions;
+type(services) -> couch_epi_functions.
+
+reload_if_updated(#state{handle = Module} = State) ->
+ case couch_epi_util:module_exists(Module) of
+ true ->
+ do_reload_if_updated(State);
+ false ->
+ {ok, State}
+ end.
+
+compile_module(State) ->
+ do_reload_if_updated(State).
+
+do_reload_if_updated(#state{} = State0) ->
+ #state{
+ hash = OldHash,
+ type = Type,
+ key = Key,
+ kind = Kind
+ } = State0,
+ Defs = couch_epi_plugin:definitions(Kind, Key),
+ case Type:data(Defs) of
+ {ok, OldHash, _Data} ->
+ {ok, State0};
+ {ok, Hash, Data} ->
+ {ok, OldData, State1} = safe_set(Hash, Data, State0),
+ notify(Key, OldData, Data, Defs),
+ State2 = update_interval(Type:interval(Defs), State1),
+ {ok, State2};
+ Else ->
+ {Else, State0}
+ end.
+
+update_interval(undefined, #state{timer = Timer} = State) ->
+ State#state{timer = cancel_timer(Timer)};
+update_interval(Interval, #state{timer = Timer} = State) ->
+ State#state{timer = start_timer(Interval, Timer)}.
+
+start_timer(Interval, {undefined, undefined}) ->
+ {ok, Timer} = timer:send_interval(Interval, self(), tick),
+ {Timer, Interval};
+start_timer(Interval, {Timer, _Interval}) ->
+ start_timer(Interval, cancel_timer(Timer)).
+
+cancel_timer({undefined, undefined}) ->
+ {undefined, undefined};
+cancel_timer({Timer, _Interval}) ->
+ timer:cancel(Timer),
+ {undefined, undefined}.
+
+safe_set(Hash, Data, #state{} = State) ->
+ #state{
+ handle = Handle,
+ codegen = CodeGen
+ } = State,
+ try
+ OldData = CodeGen:get_current_definitions(Handle),
+ ok = CodeGen:generate(Handle, Data),
+ {ok, OldData, State#state{hash = Hash}}
+ catch Class:Reason ->
+ {{Class, Reason}, State}
+ end.
+
+notify(Key, OldData, NewData, Defs) ->
+ Specs = [Spec || {_App, Spec} <- Defs],
+ couch_epi_plugin:notify(Key, OldData, NewData, Specs),
+ ok.
diff --git a/src/couch_epi_plugin.erl b/src/couch_epi_plugin.erl
new file mode 100644
index 0000000..05aa591
--- /dev/null
+++ b/src/couch_epi_plugin.erl
@@ -0,0 +1,345 @@
+% 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_plugin).
+
+-include("couch_epi.hrl").
+
+-export([
+ definitions/1,
+ definitions/2,
+ grouped_definitions/1,
+ plugin_processes/2,
+ codegen/1
+]).
+
+-export([notify/4]).
+
+%% ------------------------------------------------------------------
+%% Types Definitions
+%% ------------------------------------------------------------------
+
+-type kind()
+ :: providers
+ | data_providers
+ | services
+ | data_subscriptions
+ .
+
+-type key()
+ :: {ServiceId :: couch_epi:service_id(), Key :: couch_epi:key()}
+ | couch_epi:service_id().
+
+-callback app() -> couch_epi:app().
+-callback providers() -> [{couch_epi:service_id(), module()}].
+-callback services() -> [{couch_epi:service_id(), module()}].
+-callback data_subscriptions() -> [{couch_epi:service_id(), couch_epi:key()}].
+-callback data_providers() -> [{couch_epi:service_id(), couch_epi:data_spec()}].
+-callback processes() -> [{couch_epi:plugin_id(), [supervisor:child_spec()]}].
+-callback notify(Key :: term(), Old :: term(), New :: term()) -> ok.
+
+%% ------------------------------------------------------------------
+%% API Function Definitions
+%% ------------------------------------------------------------------
+
+definitions(Plugins) ->
+ lists:append([extract_definitions(Plugin) || Plugin <- Plugins]).
+
+plugin_processes(Plugin, Plugins) ->
+ lists:append([
+ Specs || P0 <- Plugins, {P1, Specs} <- P0:processes(), P1 =:= Plugin]).
+
+grouped_definitions(Plugins) ->
+ Defs = lists:append([extract_definitions(Plugin) || Plugin <- Plugins]),
+ group_specs(Defs).
+
+definitions(Kind, Key) ->
+ Plugins = application:get_env(couch_epi, plugins, []),
+ Definitions = definitions(Plugins),
+ Filtered = filter_by_key(Definitions, Kind, Key),
+ case group_specs(Filtered) of
+ [] -> [];
+ [{_, Defs}] -> Defs
+ end.
+
+notify(Key, OldData, NewData, Specs) ->
+ Plugins = lists:usort([Plugin || #couch_epi_spec{behaviour = Plugin} <- Specs]),
+ [notify_plugin(Plugin, Key, OldData, NewData) || Plugin <- Plugins],
+ ok.
+
+%% ------------------------------------------------------------------
+%% Internal Function Definitions
+%% ------------------------------------------------------------------
+
+notify_plugin(Plugin, Key, OldData, NewData) ->
+ App = Plugin:app(),
+ Plugin:notify(Key, app_data(App, OldData), app_data(App, NewData)).
+
+
+app_data(App, Data) ->
+ case lists:keyfind(App, 1, Data) of
+ {App, AppData} -> AppData;
+ false -> []
+ end.
+
+filter_by_key(Definitions, Kind, Key) ->
+ lists:filter(fun(Spec) -> by_key(Spec, Kind, Key) end, Definitions).
+
+by_key(#couch_epi_spec{kind = Kind, key = Key}, Kind, Key) -> true;
+by_key(_, _, _) -> false.
+
+
+extract_definitions(Plugin) ->
+ specs(Plugin, providers)
+ ++ specs(Plugin, data_providers)
+ ++ specs(Plugin, services)
+ ++ specs(Plugin, data_subscriptions).
+
+-spec group_specs(Specs :: [#couch_epi_spec{}]) -> GroupedSpecs when
+ GroupedSpecs ::
+ [{{kind(), key()}, [{couch_epi:app(), #couch_epi_spec{}}]}].
+
+group_specs(Specs) ->
+ group(
+ [{{Kind, Key}, group([{App, Spec}])}
+ || #couch_epi_spec{kind = Kind, key = Key, app = App} = Spec <- Specs]).
+
+
+group(KV) ->
+ dict:to_list(lists:foldr(fun({K,V}, D) ->
+ dict:append_list(K, V, D)
+ end, dict:new(), KV)).
+
+specs(Plugin, Kind) ->
+ [spec(parse(Spec, Kind), Plugin, Kind) || Spec <- Plugin:Kind()].
+
+spec({Key, Value, Options}, Plugin, Kind) ->
+ App = Plugin:app(),
+ #couch_epi_spec{
+ app = App,
+ behaviour = Plugin,
+ kind = Kind,
+ options = Options,
+ key = Key,
+ value = Value,
+ codegen = codegen(Kind),
+ type = type(Kind, Value)
+ }.
+
+parse({Key, Value}, Kind) ->
+ parse({Key, Value, []}, Kind);
+parse({Key, Value, Options}, data_subscriptions) ->
+ {{Key, Value}, undefined, Options};
+parse({_, _, _} = Tuple, _Kind) ->
+ Tuple.
+
+codegen(providers) -> couch_epi_functions_gen;
+codegen(services) -> couch_epi_functions_gen;
+codegen(data_providers) -> couch_epi_data_gen;
+codegen(data_subscriptions) -> couch_epi_data_gen.
+
+type(providers, _) -> couch_epi_functions;
+type(services, _) -> couch_epi_functions;
+type(data_providers, _) -> couch_epi_data;
+type(data_subscriptions, _) -> undefined.
+
+
+%% ------------------------------------------------------------------
+%% Tests
+%% ------------------------------------------------------------------
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+plugin_module(foo_epi) ->
+ "
+ -compile([export_all]).
+
+ app() -> foo.
+ providers() ->
+ [
+ {chttpd_handlers, foo_provider},
+ {bar_handlers, bar_provider}
+ ].
+
+ services() ->
+ [
+ {foo_handlers, foo_service}
+ ].
+
+ data_providers() ->
+ [
+ {{foo_service, data1}, {file, \"abs_file\"}, [{interval, 5000}]},
+ {{foo_service, data2}, {priv_file, \"priv_file\"}},
+ {{foo_service, data3}, {module, foo_data}}
+ ].
+
+ data_subscriptions() ->
+ [
+ {stats, foo_definitions}
+ ].
+
+ processes() -> [].
+
+ notify(_, _, _) -> ok.
+ ";
+plugin_module(bar_epi) ->
+ "
+ -compile([export_all]).
+
+ app() -> bar.
+ providers() ->
+ [
+ {chttpd_handlers, bar_provider},
+ {bar_handlers, bar_provider}
+ ].
+
+ services() ->
+ [
+ {bar_handlers, bar_service}
+ ].
+
+ data_providers() ->
+ [].
+
+ data_subscriptions() ->
+ [
+ {foo_service, data1}
+ ].
+
+ processes() -> [].
+
+ notify(_, _, _) -> ok.
+ ".
+
+generate_module(Name, Body) ->
+ Tokens = couch_epi_codegen:scan(Body),
+ couch_epi_codegen:generate(Name, Tokens).
+
+generate_modules(Kind, Providers) ->
+ [generate_module(P, Kind(P)) || P <- Providers].
+
+definitions_test() ->
+ Expected = lists:sort([
+ #couch_epi_spec{
+ behaviour = bar_epi,
+ app = bar,
+ kind = providers,
+ options = [],
+ key = bar_handlers,
+ value = bar_provider,
+ codegen = couch_epi_functions_gen,
+ type = couch_epi_functions
+ },
+ #couch_epi_spec{
+ behaviour = bar_epi,
+ app = bar,
+ kind = services,
+ options = [],
+ key = bar_handlers,
+ value = bar_service,
+ codegen = couch_epi_functions_gen,
+ type = couch_epi_functions
+ },
+ #couch_epi_spec{
+ behaviour = bar_epi,
+ app = bar,
+ kind = providers,
+ options = [],
+ key = chttpd_handlers,
+ value = bar_provider,
+ codegen = couch_epi_functions_gen,
+ type = couch_epi_functions
+ },
+ #couch_epi_spec{
+ behaviour = bar_epi,
+ app = bar,
+ kind = data_subscriptions,
+ options = [],
+ key = {foo_service, data1},
+ value = undefined,
+ codegen = couch_epi_data_gen
+ },
+ #couch_epi_spec{
+ behaviour = foo_epi,
+ app = foo,
+ kind = providers,
+ options = [],
+ key = bar_handlers,
+ value = bar_provider,
+ codegen = couch_epi_functions_gen,
+ type = couch_epi_functions
+ },
+ #couch_epi_spec{
+ behaviour = foo_epi,
+ app = foo,
+ kind = providers,
+ options = [],
+ key = chttpd_handlers,
+ value = foo_provider,
+ codegen = couch_epi_functions_gen,
+ type = couch_epi_functions},
+ #couch_epi_spec{
+ behaviour = foo_epi,
+ app = foo,
+ kind = services,
+ options = [],
+ key = foo_handlers,
+ value = foo_service,
+ codegen = couch_epi_functions_gen,
+ type = couch_epi_functions},
+ #couch_epi_spec{
+ behaviour = foo_epi,
+ app = foo,
+ kind = data_providers,
+ options = [{interval, 5000}],
+ key = {foo_service, data1},
+ value = {file,"abs_file"},
+ codegen = couch_epi_data_gen,
+ type = couch_epi_data
+ },
+ #couch_epi_spec{
+ behaviour = foo_epi,
+ app = foo,
+ kind = data_providers,
+ options = [],
+ key = {foo_service, data2},
+ value = {priv_file, "priv_file"},
+ codegen = couch_epi_data_gen,
+ type = couch_epi_data
+ },
+ #couch_epi_spec{
+ behaviour = foo_epi,
+ app = foo,
+ kind = data_providers,
+ options = [],
+ key = {foo_service, data3},
+ value = {module, foo_data},
+ codegen = couch_epi_data_gen,
+ type = couch_epi_data
+ },
+ #couch_epi_spec{
+ behaviour = foo_epi,
+ app = foo,
+ kind = data_subscriptions,
+ options = [],
+ key = {stats, foo_definitions},
+ value = undefined,
+ codegen = couch_epi_data_gen
+ }
+ ]),
+
+ [ok,ok] = generate_modules(fun plugin_module/1, [foo_epi, bar_epi]),
+ Tests = lists:zip(Expected, lists:sort(definitions([foo_epi, bar_epi]))),
+ [?assertEqual(Expect, Result) || {Expect, Result} <- Tests],
+ ok.
+-endif.
diff --git a/src/couch_epi_server.erl b/src/couch_epi_server.erl
deleted file mode 100644
index 19d8eb4..0000000
--- a/src/couch_epi_server.erl
+++ /dev/null
@@ -1,144 +0,0 @@
-% 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_server).
--behaviour(gen_server).
--define(SERVER, ?MODULE).
-
-%% ------------------------------------------------------------------
-%% API Function Exports
-%% ------------------------------------------------------------------
-
--export([start_link/0]).
--export([subscribe/3, subscribe/4, unsubscribe/1, unsubscribe/2]).
--export([notify/4, notify/5]).
-
-%% ------------------------------------------------------------------
-%% gen_server Function Exports
-%% ------------------------------------------------------------------
-
--export([init/1, handle_call/3, handle_cast/2, handle_info/2,
- terminate/2, code_change/3]).
-
--record(epi_server_state, {subscriptions}).
-
-%% ------------------------------------------------------------------
-%% API Function Definitions
-%% ------------------------------------------------------------------
-
-start_link() ->
- gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-
-subscribe(App, Key, MFA) ->
- subscribe(?SERVER, App, Key, MFA).
-
-subscribe(Server, App, Key, {_M, _F, _A} = MFA) ->
- gen_server:call(Server, {subscribe, App, Key, MFA}).
-
-unsubscribe(Subscription) ->
- unsubscribe(?SERVER, Subscription).
-
-unsubscribe(Server, Subscription) ->
- gen_server:call(Server, {unsubscribe, Subscription}).
-
-notify(App, Key, OldData, Data) ->
- notify(?SERVER, App, Key, OldData, Data).
-
-notify(Server, App, Key, OldData, Data) ->
- gen_server:cast(Server, {notify, App, Key, OldData, Data}).
-
-
-%% ------------------------------------------------------------------
-%% gen_server Function Definitions
-%% ------------------------------------------------------------------
-
-init(_Args) ->
- State = #epi_server_state{subscriptions = dict:new()},
- {ok, State}.
-
-handle_call({subscribe, App, Key, MFA}, {Pid, _Tag},
- #epi_server_state{subscriptions = Subscriptions0} = State0) ->
- {Subscription, Subscriptions1} = add(Pid, Subscriptions0, App, Key, MFA),
- State1 = State0#epi_server_state{subscriptions = Subscriptions1},
- {reply, {ok, Subscription}, State1};
-handle_call({unsubscribe, Subscription}, _From,
- #epi_server_state{subscriptions = Subscriptions0} = State0) ->
- Subscriptions1 = remove(Subscriptions0, Subscription),
- State1 = State0#epi_server_state{subscriptions = Subscriptions1},
- {reply, ok, State1};
-handle_call(_Request, _From, State) ->
- {stop, normal, State}.
-
-handle_cast({notify, App, Key, OldData, Data},
- #epi_server_state{subscriptions = Subscriptions} = State) ->
- Subscribers = subscribers(Subscriptions, App, Key),
- notify_subscribers(Subscribers, App, Key, OldData, Data),
- {noreply, State};
-handle_cast(_Msg, State) ->
- {noreply, State}.
-
-handle_info({'DOWN', MonitorRef, _Type, _Object, _Info},
- #epi_server_state{subscriptions = Subscriptions0} = State0) ->
- Subscriptions1 = remove(Subscriptions0, MonitorRef),
- State1 = State0#epi_server_state{subscriptions = Subscriptions1},
- {noreply, State1};
-handle_info(_Info, State) ->
- {noreply, State}.
-
-terminate(_Reason, _State) ->
- ok.
-
-code_change(_OldVsn, State, _Extra) ->
- {ok, State}.
-
-%% ------------------------------------------------------------------
-%% Internal Function Definitions
-%% ------------------------------------------------------------------
-
-subscribers(Subscriptions, App, Key) ->
- case dict:find({App, Key}, Subscriptions) of
- error ->
- [];
- {ok, Subscribers} ->
- Subscribers
- end.
-
-add(Pid, Subscriptions, App, Key, MFA) ->
- Subscription = erlang:monitor(process, Pid),
- {Subscription, dict:append({App, Key}, {Subscription, MFA}, Subscriptions)}.
-
-remove(Subscriptions, SubscriptionId) ->
- case find(Subscriptions, SubscriptionId) of
- {App, Key} ->
- demonitor(SubscriptionId, [flush]),
- delete_subscriber(Subscriptions, App, Key, SubscriptionId);
- _ ->
- Subscriptions
- end.
-
-find(Subscriptions, SubscriptionId) ->
- dict:fold(fun(Key, Subscribers, Acc) ->
- case [ok || {Id, _MFA} <- Subscribers, Id =:= SubscriptionId] of
- [_] ->
- Key;
- [] ->
- Acc
- end
- end, not_found, Subscriptions).
-
-delete_subscriber(Subscriptions, App, Key, SubscriptionId) ->
- dict:update({App, Key}, fun(Subscribers) ->
- [{Id, MFA} || {Id, MFA} <- Subscribers, Id =/= SubscriptionId]
- end, Subscriptions).
-
-notify_subscribers(Subscribers, App, Key, OldData, Data) ->
- [M:F(App, Key, OldData, Data, A) || {_Id, {M, F, A}} <- Subscribers].
diff --git a/src/couch_epi_sup.erl b/src/couch_epi_sup.erl
index 3c35d2d..77d81b4 100644
--- a/src/couch_epi_sup.erl
+++ b/src/couch_epi_sup.erl
@@ -12,18 +12,33 @@
-module(couch_epi_sup).
+%% ----------------------------------
+%% Important assumption
+%% ====================
+%% Keeper and codechange_monitor childspecs relie on underdocumented feature.
+%% 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/1]).
%% Supervisor callbacks
-export([init/1]).
%% Helper macro for declaring children of supervisor
-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
--define(SUP(I, A),
- {I, {I, start_link, A}, permanent, infinity, supervisor, [I]}).
%% ===================================================================
%% API functions
@@ -32,13 +47,173 @@
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+plugin_childspecs(Plugin) ->
+ Plugins = application:get_env(couch_epi, plugins, []),
+ plugin_childspecs(Plugin, Plugins).
+
%% ===================================================================
%% Supervisor callbacks
%% ===================================================================
init([]) ->
- Children = [
- ?CHILD(couch_epi_server, worker),
- ?SUP(couch_epi_keeper_sup, [])
+ {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) ->
+ Definitions = couch_epi_plugin:grouped_definitions([Plugin]),
+ ExtraChildren = couch_epi_plugin:plugin_processes(Plugin, Plugins),
+ ExtraChildren ++ 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].
+
+
+%% ------------------------------------------------------------------
+%% 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)]}
+ ].
+
+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]},
+ {{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]}
+ ]),
+ Children = lists:sort(plugin_childspecs(?MODULE, [?MODULE])),
+ Results = [
+ {parse_child_id(Id), Args, lists:sort(Modules)}
+ || {Id, {_M, _F, Args}, _, _, _, Modules} <- Children
],
- {ok, { {one_for_one, 5, 10}, Children} }.
+
+ Tests = lists:zip(Expected, Results),
+ [?assertEqual(Expect, Result) || {Expect, Result} <- Tests],
+
+ ok.
+
+-endif.
diff --git a/src/couch_epi_util.erl b/src/couch_epi_util.erl
index c65ea4a..5020fba 100644
--- a/src/couch_epi_util.erl
+++ b/src/couch_epi_util.erl
@@ -12,7 +12,7 @@
-module(couch_epi_util).
--export([module_version/1, hash/1, md5/1]).
+-export([module_version/1, hash/1, md5/1, module_exists/1]).
-compile([nowarn_deprecated_function]).
@@ -23,7 +23,7 @@
hash(Term) ->
<<SigInt:128/integer>> = md5(term_to_binary(Term)),
- io_lib:format("\"~.36B\"",[SigInt]).
+ lists:flatten(io_lib:format("\"~.36B\"",[SigInt])).
md5(Data) ->
case erlang:function_exported(crypto, hash, 2) of
@@ -32,3 +32,6 @@
false ->
crypto:md5(Data)
end.
+
+module_exists(Module) ->
+ erlang:function_exported(Module, module_info, 0).
diff --git a/test/couch_epi_tests.erl b/test/couch_epi_tests.erl
index a43ade9..fc2daa8 100644
--- a/test/couch_epi_tests.erl
+++ b/test/couch_epi_tests.erl
@@ -17,12 +17,18 @@
-define(DATA_FILE1, ?ABS_PATH("test/fixtures/app_data1.cfg")).
-define(DATA_FILE2, ?ABS_PATH("test/fixtures/app_data2.cfg")).
--export([notify_cb/5, save/3]).
+-export([notify_cb/4, save/3]).
--record(ctx, {file, handle, pid, kv, key}).
+-record(ctx, {file, handle, pid, kv, key, modules = []}).
-define(TIMEOUT, 5000).
+-define(temp_atom,
+ fun() ->
+ {A, B, C} = erlang:now(),
+ list_to_atom(lists:flatten(io_lib:format("~p~p~p", [A, B, C])))
+ end).
+
-define(MODULE1(Name), "
-export([inc/2, fail/2]).
@@ -76,72 +82,142 @@
].
").
-notify_cb(App, Key, OldData, Data, KV) ->
- save(KV, is_called, {App, Key, OldData, Data}).
+%% ------------------------------------------------------------------
+%% couch_epi_plugin behaviour
+%% ------------------------------------------------------------------
+
+plugin_module([KV, Spec]) ->
+ SpecStr = io_lib:format("~w", [Spec]),
+ KVStr = "'" ++ atom_to_list(KV) ++ "'",
+ "
+ -compile([export_all]).
+
+ app() -> test_app.
+ providers() ->
+ [].
+
+ services() ->
+ [].
+
+ data_providers() ->
+ [
+ {{test_app, descriptions}, " ++ SpecStr ++ ", [{interval, 100}]}
+ ].
+
+ data_subscriptions() ->
+ [
+ {test_app, descriptions}
+ ].
+
+ processes() -> [].
+
+ notify(Key, OldData, Data) ->
+ couch_epi_tests:notify_cb(Key, OldData, Data, " ++ KVStr ++ ").
+ ";
+plugin_module([KV]) ->
+ KVStr = "'" ++ atom_to_list(KV) ++ "'",
+ "
+ -compile([export_all]).
+
+ app() -> test_app.
+ providers() ->
+ [
+ {my_service, provider1},
+ {my_service, provider2}
+ ].
+
+ services() ->
+ [
+ {my_service, provider1}
+ ].
+
+ data_providers() ->
+ [].
+
+ data_subscriptions() ->
+ [].
+
+ processes() -> [].
+
+ notify(Key, OldData, Data) ->
+ couch_epi_tests:notify_cb(Key, OldData, Data, " ++ KVStr ++ ").
+ ".
-setup(couch_epi_data_source) ->
+notify_cb(Key, OldData, Data, KV) ->
+ save(KV, is_called, {Key, OldData, Data}).
+
+start_epi(Plugins) ->
+ application:load(couch_epi),
+ PluginsModules = lists:map(fun({Module, Body}) ->
+ ok = generate_module(Module, Body),
+ Module
+ end, Plugins),
+ application:set_env(couch_epi, plugins, PluginsModules),
+ application:start(couch_epi).
+
+setup(data_file) ->
error_logger:tty(false),
Key = {test_app, descriptions},
File = ?tempfile(),
{ok, _} = file:copy(?DATA_FILE1, File),
- application:start(couch_epi),
- {ok, Pid} = couch_epi_data_source:start_link(
- test_app, {epi_key, Key}, {file, File}, [{interval, 100}]),
- ok = couch_epi_data_source:wait(Pid),
- KV = state_storage(),
- ok = couch_epi:register_service(Key),
+ KV = start_state_storage(),
+
+ ok = start_epi([{provider_epi, plugin_module([KV, {file, File}])}]),
+
+ Pid = whereis(couch_epi:get_handle(Key)),
+
+
#ctx{
file = File,
key = Key,
handle = couch_epi:get_handle(Key),
kv = KV,
pid = Pid};
-setup(couch_epi_data) ->
+setup(data_module) ->
error_logger:tty(false),
Key = {test_app, descriptions},
- application:start(couch_epi),
- ok = generate_module(provider, ?DATA_MODULE1(provider)),
- {ok, Pid} = couch_epi_data:start_link(
- test_app, {epi_key, Key}, provider, []),
- ok = couch_epi_data:wait(Pid),
- KV = state_storage(),
- ok = couch_epi:register_service(Key),
+ ok = generate_module(provider, ?DATA_MODULE1(provider)),
+ KV = start_state_storage(),
+
+ ok = start_epi([{provider_epi, plugin_module([KV, {module, provider}])}]),
+
+ Pid = whereis(couch_epi:get_handle(Key)),
+ Handle = couch_epi:get_handle(Key),
+
#ctx{
key = Key,
- handle = couch_epi:get_handle(Key),
+ handle = Handle,
+ modules = [Handle, provider],
kv = KV,
pid = Pid};
-setup(couch_epi_functions) ->
+setup(functions) ->
Key = my_service,
error_logger:tty(false),
- application:start(couch_epi),
ok = generate_module(provider1, ?MODULE1(provider1)),
ok = generate_module(provider2, ?MODULE2(provider2)),
- {ok, Pid} = couch_epi_functions:start_link(
- test_app, {epi_key, Key}, {modules, [provider1, provider2]},
- [{interval, 100}]),
- ok = couch_epi_functions:wait(Pid),
- KV = state_storage(),
- ok = couch_epi:register_service(Key),
+ KV = start_state_storage(),
+
+ ok = start_epi([{provider_epi, plugin_module([KV])}]),
+
+ Pid = whereis(couch_epi:get_handle(Key)),
+ Handle = couch_epi:get_handle(Key),
+
#ctx{
key = Key,
- handle = couch_epi:get_handle(Key),
+ handle = Handle,
+ modules = [Handle, provider1, provider2],
kv = KV,
pid = Pid};
-setup(_Opts) ->
- setup(couch_epi_functions).
+setup({options, _Opts}) ->
+ setup(functions).
-teardown(Module, #ctx{pid = Pid} = Ctx) when is_atom(Module) ->
- Module:stop(Pid),
- teardown(Ctx);
-teardown(_Opts, #ctx{pid = Pid} = Ctx) ->
- couch_epi_functions:stop(Pid),
+teardown(_Case, #ctx{} = Ctx) ->
teardown(Ctx).
teardown(#ctx{file = File} = Ctx) when File /= undefined ->
@@ -152,26 +228,25 @@
application:stop(couch_epi),
ok.
-upgrade_release(Pid, Module) ->
+upgrade_release(Pid, Modules) ->
sys:suspend(Pid),
- 'ok' = sys:change_code(Pid, Module, 'undefined', []),
+ [ok = sys:change_code(Pid, M, undefined, []) || M <- Modules],
sys:resume(Pid),
ok.
epi_config_update_test_() ->
Funs = [
fun ensure_notified_when_changed/2,
- fun ensure_not_notified_when_no_change/2,
- fun ensure_not_notified_when_unsubscribed/2
+ fun ensure_not_notified_when_no_change/2
],
- Modules= [
- couch_epi_data,
- couch_epi_data_source,
- couch_epi_functions
+ Cases = [
+ data_file,
+ data_module,
+ functions
],
{
"config update tests",
- [make_case("Check notifications for: ", Modules, Funs)]
+ [make_case("Check notifications for: ", Cases, Funs)]
}.
epi_data_source_test_() ->
@@ -184,13 +259,13 @@
fun check_keys/2,
fun check_subscribers/2
],
- Modules= [
- couch_epi_data,
- couch_epi_data_source
+ Cases = [
+ data_file,
+ data_module
],
{
"epi data API tests",
- [make_case("Check query API for: ", Modules, Funs)]
+ [make_case("Check query API for: ", Cases, Funs)]
}.
@@ -199,7 +274,7 @@
"epi dispatch tests",
{
foreach,
- fun() -> setup(couch_epi_functions) end,
+ fun() -> setup(functions) end,
fun teardown/1,
[
fun check_pipe/1,
@@ -210,27 +285,11 @@
}
}.
-
-epi_subscription_test_() ->
- Funs = [
- fun ensure_unsubscribe_when_caller_die/2
- ],
- Modules= [
- couch_epi_data,
- couch_epi_data_source,
- couch_epi_functions
- ],
- {
- "epi subscription tests",
- [make_case("Check subscription API for: ", Modules, Funs)]
- }.
-
-
epi_reload_test_() ->
- Modules= [
- couch_epi_data,
- couch_epi_data_source,
- couch_epi_functions
+ Cases = [
+ data_file,
+ data_module,
+ functions
],
Funs = [
fun ensure_reload_if_manually_triggered/2,
@@ -239,30 +298,39 @@
],
{
"epi reload tests",
- {
- foreachx,
- fun setup/1,
- fun teardown/2,
- [{M, Fun} || M <- Modules, Fun <- Funs]
- }
+ [make_case("Check reload for: ", Cases, Funs)]
}.
-
apply_options_test_() ->
Funs = [fun ensure_apply_is_called/2],
- make_case("Apply with options: ", valid_options_permutations(), Funs).
+ Setups = {options, valid_options_permutations()},
+ {
+ "apply options tests",
+ [make_case("Apply with options: ", Setups, Funs)]
+ }.
+make_case(Msg, {Tag, P}, Funs) ->
+ Cases = [{Tag, Case} || Case <- P],
+ make_case(Msg, Cases, Funs);
make_case(Msg, P, Funs) ->
[{format_case_name(Msg, Case), [
{
foreachx, fun setup/1, fun teardown/2,
[
- {Case, Fun} || Fun <- Funs
+ {Case, make_fun(Fun, 2)} || Fun <- Funs
]
}
]} || Case <- P].
+make_fun(Fun, Arity) ->
+ {arity, A} = lists:keyfind(arity, 1, erlang:fun_info(Fun)),
+ make_fun(Fun, Arity, A).
+
+make_fun(Fun, A, A) -> Fun;
+make_fun(Fun, 2, 1) -> fun(_, A) -> Fun(A) end;
+make_fun(Fun, 1, 2) -> fun(A) -> Fun(undefined, A) end.
+
format_case_name(Msg, Case) ->
lists:flatten(Msg ++ io_lib:format("~p", [Case])).
@@ -276,61 +344,54 @@
[concurrent, ignore_errors]
].
-ensure_notified_when_changed(couch_epi_functions, #ctx{key = Key} = Ctx) ->
+ensure_notified_when_changed(functions, #ctx{key = Key} = Ctx) ->
?_test(begin
subscribe(Ctx, test_app, Key),
- update(couch_epi_functions, Ctx),
+ update(functions, Ctx),
timer:sleep(200),
Result = get(Ctx, is_called),
- Expected = {test_app, Key,
- {modules, [provider1, provider2]},
- {modules, [provider1, provider2]}},
- ?assertMatch({ok, Expected}, Result),
+ ExpectedDefs = [
+ {provider1,[{inc,2},{fail,2}]},
+ {provider2,[{inc,2},{fail,2}]}
+ ],
+ ?assertEqual({ok, {Key, ExpectedDefs, ExpectedDefs}}, Result),
ok
end);
-ensure_notified_when_changed(Module, #ctx{key = Key} = Ctx) ->
+ensure_notified_when_changed(Case, #ctx{key = Key} = Ctx) ->
?_test(begin
subscribe(Ctx, test_app, Key),
- update(Module, Ctx),
+ update(Case, Ctx),
timer:sleep(200),
ExpectedData = lists:usort([
{[complex, key, 1], [{type, counter}, {desc, updated_foo}]},
{[complex, key, 2], [{type, counter}, {desc, bar}]}
]),
Result = get(Ctx, is_called),
- ?assertMatch({ok, {test_app, Key, {data, _}, {data, _}}}, Result),
- {ok, {test_app, Key, {data, OldData}, {data, Data}}} = Result,
+ ?assertMatch({ok, {Key, _OldData, _Data}}, Result),
+ {ok, {Key, OldData, Data}} = Result,
?assertMatch(ExpectedData, lists:usort(Data)),
?assertMatch(
[{[complex, key, 1], [{type, counter}, {desc, foo}]}],
lists:usort(OldData))
end).
-ensure_not_notified_when_no_change(_Module, #ctx{key = Key} = Ctx) ->
+ensure_not_notified_when_no_change(_Case, #ctx{key = Key} = Ctx) ->
?_test(begin
subscribe(Ctx, test_app, Key),
timer:sleep(200),
?assertMatch(error, get(Ctx, is_called))
end).
-ensure_not_notified_when_unsubscribed(Module, #ctx{key = Key} = Ctx) ->
- ?_test(begin
- SubscriptionId = subscribe(Ctx, test_app, Key),
- couch_epi:unsubscribe(SubscriptionId),
- timer:sleep(100),
- update(Module, Ctx),
- timer:sleep(200),
- ?assertMatch(error, get(Ctx, is_called))
- end).
-
-ensure_apply_is_called(Opts, #ctx{handle = Handle, kv = KV, key = Key} = Ctx) ->
+ensure_apply_is_called({options, Opts}, #ctx{handle = Handle, kv = KV, key = Key} = Ctx) ->
?_test(begin
couch_epi:apply(Handle, Key, inc, [KV, 2], Opts),
maybe_wait(Opts),
?assertMatch({ok, _}, get(Ctx, inc1)),
?assertMatch({ok, _}, get(Ctx, inc2)),
ok
- end).
+ end);
+ensure_apply_is_called(undefined, #ctx{} = Ctx) ->
+ ensure_apply_is_called({options, []}, Ctx).
check_pipe(#ctx{handle = Handle, kv = KV, key = Key}) ->
?_test(begin
@@ -361,42 +422,32 @@
ok
end).
-ensure_unsubscribe_when_caller_die(_Module, #ctx{key = Key} = Ctx) ->
- ?_test(begin
- spawn(fun() ->
- subscribe(Ctx, test_app, Key)
- end),
- timer:sleep(200),
- ?assertMatch(error, get(Ctx, is_called))
- end).
-
-
pipe_state(Ctx) ->
Trace = [get(Ctx, inc1), get(Ctx, inc2)],
lists:usort([State || {ok, State} <- Trace]).
-check_dump(_Module, #ctx{handle = Handle}) ->
+check_dump(_Case, #ctx{handle = Handle}) ->
?_test(begin
?assertMatch(
[[{type, counter}, {desc, foo}]],
couch_epi:dump(Handle))
end).
-check_get(_Module, #ctx{handle = Handle}) ->
+check_get(_Case, #ctx{handle = Handle}) ->
?_test(begin
?assertMatch(
[[{type, counter}, {desc, foo}]],
couch_epi:get(Handle, [complex,key, 1]))
end).
-check_get_value(_Module, #ctx{handle = Handle}) ->
+check_get_value(_Case, #ctx{handle = Handle}) ->
?_test(begin
?assertMatch(
[{type, counter}, {desc, foo}],
couch_epi:get_value(Handle, test_app, [complex,key, 1]))
end).
-check_by_key(_Module, #ctx{handle = Handle}) ->
+check_by_key(_Case, #ctx{handle = Handle}) ->
?_test(begin
?assertMatch(
[{[complex, key, 1],
@@ -407,7 +458,7 @@
couch_epi:by_key(Handle, [complex, key, 1]))
end).
-check_by_source(_Module, #ctx{handle = Handle}) ->
+check_by_source(_Case, #ctx{handle = Handle}) ->
?_test(begin
?assertMatch(
[{test_app,
@@ -418,65 +469,60 @@
couch_epi:by_source(Handle, test_app))
end).
-check_keys(_Module, #ctx{handle = Handle}) ->
+check_keys(_Case, #ctx{handle = Handle}) ->
?_assertMatch([[complex,key,1]], couch_epi:keys(Handle)).
-check_subscribers(_Module, #ctx{handle = Handle}) ->
+check_subscribers(_Case, #ctx{handle = Handle}) ->
?_assertMatch([test_app], couch_epi:subscribers(Handle)).
-ensure_reload_if_manually_triggered(Module, #ctx{pid = Pid, key = Key} = Ctx) ->
+ensure_reload_if_manually_triggered(Case, #ctx{pid = Pid, key = Key} = Ctx) ->
?_test(begin
subscribe(Ctx, test_app, Key),
- update_definitions(Module, Ctx),
- Module:reload(Pid),
+ update_definitions(Case, Ctx),
+ couch_epi_module_keeper:reload(Pid),
timer:sleep(50),
- Result = get(Ctx, is_called),
- ?assertNotMatch(error, Result)
+ ?assertNotEqual(error, get(Ctx, is_called))
end).
-ensure_reload_if_changed(couch_epi_data_source = Module,
+ensure_reload_if_changed(data_file = Case,
#ctx{key = Key, handle = Handle} = Ctx) ->
?_test(begin
Version = Handle:version(),
subscribe(Ctx, test_app, Key),
- update_definitions(Module, Ctx),
+ update_definitions(Case, Ctx),
timer:sleep(250),
?assertNotEqual(Version, Handle:version()),
- Result = get(Ctx, is_called),
- ?assertNotMatch(error, Result)
+ ?assertNotEqual(error, get(Ctx, is_called))
end);
-ensure_reload_if_changed(Module,
+ensure_reload_if_changed(Case,
#ctx{key = Key, handle = Handle} = Ctx) ->
?_test(begin
Version = Handle:version(),
subscribe(Ctx, test_app, Key),
- update(Module, Ctx),
+ update(Case, Ctx),
?assertNotEqual(Version, Handle:version()),
timer:sleep(100), %% Allow some time for notify to be called
- Result = get(Ctx, is_called),
- ?assertNotMatch(error, Result)
+ ?assertNotEqual(error, get(Ctx, is_called))
end).
-ensure_no_reload_when_no_change(couch_epi_functions = Module,
- #ctx{pid = Pid, key = Key, handle = Handle} = Ctx) ->
+ensure_no_reload_when_no_change(functions,
+ #ctx{pid = Pid, key = Key, handle = Handle, modules = Modules} = Ctx) ->
?_test(begin
Version = Handle:version(),
subscribe(Ctx, test_app, Key),
- upgrade_release(Pid, Module),
+ upgrade_release(Pid, Modules),
?assertEqual(Version, Handle:version()),
- Result = get(Ctx, is_called),
- ?assertMatch(error, Result)
+ ?assertEqual(error, get(Ctx, is_called))
end);
-ensure_no_reload_when_no_change(Module,
+ensure_no_reload_when_no_change(_Case,
#ctx{key = Key, handle = Handle} = Ctx) ->
?_test(begin
Version = Handle:version(),
subscribe(Ctx, test_app, Key),
timer:sleep(450),
?assertEqual(Version, Handle:version()),
- Result = get(Ctx, is_called),
- ?assertMatch(error, Result)
+ ?assertEqual(error, get(Ctx, is_called))
end).
@@ -488,24 +534,21 @@
Tokens = couch_epi_codegen:scan(Body),
couch_epi_codegen:generate(Name, Tokens).
-update(Module, #ctx{pid = Pid} = Ctx) ->
- update_definitions(Module, Ctx),
- upgrade_release(Pid, Module).
+update(Case, #ctx{pid = Pid, modules = Modules} = Ctx) ->
+ update_definitions(Case, Ctx),
+ upgrade_release(Pid, Modules).
-update_definitions(couch_epi_data_source, #ctx{file = File}) ->
+update_definitions(data_file, #ctx{file = File}) ->
{ok, _} = file:copy(?DATA_FILE2, File),
ok;
-update_definitions(couch_epi_data, #ctx{}) ->
+update_definitions(data_module, #ctx{}) ->
ok = generate_module(provider, ?DATA_MODULE2(provider));
-update_definitions(couch_epi_functions, #ctx{}) ->
+update_definitions(functions, #ctx{}) ->
ok = generate_module(provider1, ?MODULE2(provider1)).
-
-
-subscribe(#ctx{kv = Kv}, App, Key) ->
- {ok, Pid} = couch_epi:subscribe(App, Key, ?MODULE, notify_cb, Kv),
+subscribe(#ctx{kv = Kv}, _App, _Key) ->
call(Kv, empty),
- Pid.
+ ok.
maybe_wait(Opts) ->
case lists:member(concurrent, Opts) of
@@ -537,6 +580,12 @@
reply({Ref, From}, Msg) ->
From ! {reply, Ref, Msg}.
+start_state_storage() ->
+ Pid = state_storage(),
+ Name = ?temp_atom(),
+ register(Name, Pid),
+ Name.
+
state_storage() ->
spawn_link(fun() -> state_storage(dict:new()) end).