blob: 94ab0d4a8662ed46e8e7ef63131c91163f84ae41 [file]
% 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_event_server).
-behaviour(gen_server).
-export([
start_link/0
]).
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2
]).
-record(st, {
by_pid,
by_dbname
}).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, nil, []).
init(_) ->
erlang:process_flag(message_queue_data, off_heap),
{ok, #st{
by_pid = #{},
by_dbname = #{}
}}.
handle_call({register, Pid, NewDbNames}, _From, St) ->
case maps:get(Pid, St#st.by_pid, undefined) of
undefined ->
NewRef = monitor(process, Pid),
{reply, ok, register(St, NewRef, Pid, NewDbNames)};
{ReuseRef, OldDbNames} ->
unregister(St, Pid, OldDbNames),
{reply, ok, register(St, ReuseRef, Pid, NewDbNames)}
end;
handle_call({unregister, Pid}, _From, #st{by_pid = ByPid} = St) ->
case maps:get(Pid, ByPid, undefined) of
undefined ->
{reply, not_registered, St};
{Ref, OldDbNames} ->
demonitor(Ref, [flush]),
{reply, ok, unregister(St, Pid, OldDbNames)}
end;
handle_call(Msg, From, St) ->
couch_log:notice("~s ignoring call ~w from ~w", [?MODULE, Msg, From]),
{reply, ignored, St}.
handle_cast({notify, DbName, Event}, St) ->
notify_listeners(St#st.by_dbname, DbName, Event),
{noreply, St};
handle_cast(Msg, St) ->
couch_log:notice("~s ignoring cast ~w", [?MODULE, Msg]),
{noreply, St}.
handle_info({'DOWN', Ref, process, Pid, _Reason}, #st{by_pid = ByPid} = St) ->
#{Pid := {Ref, OldDbNames}} = ByPid,
{noreply, unregister(St, Pid, OldDbNames)};
handle_info(Msg, St) ->
couch_log:notice("~s ignoring info ~w", [?MODULE, Msg]),
{noreply, St}.
notify_listeners(#{} = ByDbName, DbName, Event) ->
Msg = {'$couch_event', DbName, Event},
notify_listeners(maps:get(all_dbs, ByDbName, undefined), Msg),
notify_listeners(maps:get(DbName, ByDbName, undefined), Msg).
notify_listeners(undefined, _) ->
ok;
notify_listeners(#{} = Listeners, Msg) ->
maps:foreach(fun(Pid, _) -> Pid ! Msg end, Listeners).
register(#st{by_pid = ByPid, by_dbname = ByDbName} = St, Ref, Pid, DbNames) ->
FoldFun = fun(DbName, Acc) -> add_listener(Acc, DbName, Pid) end,
ByDbName1 = lists:foldl(FoldFun, ByDbName, DbNames),
St#st{by_pid = ByPid#{Pid => {Ref, DbNames}}, by_dbname = ByDbName1}.
add_listener(#{} = ByDbName, DbName, Pid) ->
case maps:get(DbName, ByDbName, not_found) of
#{} = Listeners -> ByDbName#{DbName => Listeners#{Pid => nil}};
not_found -> ByDbName#{DbName => #{Pid => nil}}
end.
unregister(#st{by_pid = ByPid, by_dbname = ByDbName} = St, Pid, OldDbNames) ->
FoldFun = fun(DbName, Acc) -> rem_listener(Acc, DbName, Pid) end,
ByDbName1 = lists:foldl(FoldFun, ByDbName, OldDbNames),
St#st{by_pid = maps:remove(Pid, ByPid), by_dbname = ByDbName1}.
rem_listener(#{} = ByDbName, DbName, Pid) ->
#{DbName := Listeners} = ByDbName,
Listeners1 = maps:remove(Pid, Listeners),
case map_size(Listeners1) of
0 -> maps:remove(DbName, ByDbName);
_ -> ByDbName#{DbName := Listeners1}
end.
-ifdef(TEST).
-include_lib("couch/include/couch_eunit.hrl").
couch_event_test_() ->
{
foreach,
fun setup/0,
fun teardown/1,
[
?TDEF_FE(t_register_proc_basic),
?TDEF_FE(t_unregister_proc_basic),
?TDEF_FE(t_unregister_on_death),
?TDEF_FE(t_notify_basic),
?TDEF_FE(t_notify_all_dbs),
?TDEF_FE(t_register_multiple),
?TDEF_FE(t_reregister),
?TDEF_FE(t_invalid_gen_server_messages)
]
}.
setup() ->
meck:expect(couch_log, notice, fun(_, _) -> ok end),
{ok, SPid} = start_link(),
Listeners = [start_listener(), start_listener(), start_listener()],
{SPid, Listeners}.
teardown({SPid, Listeners}) ->
[kill_sync(Pid) || Pid <- [SPid | Listeners]],
meck:unload(),
ok.
t_register_proc_basic({_, [LPid | _]}) ->
?assertEqual(ok, reg(LPid, [db])),
#st{by_pid = ByPid, by_dbname = ByName} = state(),
?assertMatch(#{LPid := {_, [db]}}, ByPid),
?assertMatch(#{db := #{LPid := nil}}, ByName).
t_unregister_proc_basic({_, [LPid | _]}) ->
?assertEqual(ok, reg(LPid, [db])),
#st{by_pid = ByPid, by_dbname = ByName} = state(),
?assertMatch(#{LPid := {_, [db]}}, ByPid),
?assertMatch(#{db := #{LPid := nil}}, ByName),
?assertEqual(not_registered, unreg(self())),
?assertEqual(ok, unreg(LPid)),
#st{by_pid = #{}, by_dbname = #{}} = state().
t_unregister_on_death({_, [LPid | _]}) ->
?assertEqual(ok, reg(LPid, [db])),
#st{by_pid = ByPid, by_dbname = ByName} = state(),
?assertMatch(#{LPid := {_, [db]}}, ByPid),
?assertMatch(#{db := #{LPid := nil}}, ByName),
kill_sync(LPid),
test_util:wait(fun() ->
#st{by_pid = Pids} = state(),
case map_size(Pids) of
0 -> ok;
N when is_integer(N), N > 1 -> wait
end
end),
?assertEqual(#st{by_pid = #{}, by_dbname = #{}}, state()).
t_notify_basic({_, [LPid | _]}) ->
reg(LPid, [db]),
notify(other_db, foo),
notify(db, bar),
?assertEqual([{db, bar}], wait(LPid, 1)).
t_notify_all_dbs({_, [LPid | _]}) ->
ok = reg(LPid, [all_dbs]),
notify(db, bar),
?assertEqual([{db, bar}], wait(LPid, 1)).
t_register_multiple({_, [L1, L2, L3 | _]}) ->
reg(L1, [dbx, dby]),
reg(L2, [dby, all_dbs]),
reg(L3, [dbx, all_dbs]),
% all_dbs broadcast works
notify(dbw, e1),
?assertEqual([{dbw, e1}], wait(L2, 1)),
?assertEqual([{dbw, e1}], wait(L3, 1)),
% dbx updated, only dbx and all_dbs are notified
notify(dbx, e2),
?assertEqual([{dbx, e2}], wait(L1, 1)),
?assertEqual([{dbx, e2}], wait(L2, 1)),
% L3 gets 2 notifications once for all_dbs and dbx
?assertEqual([{dbx, e2}, {dbx, e2}], wait(L3, 2)),
unreg(L1),
unreg(L2),
unreg(L3),
#st{by_pid = #{}, by_dbname = #{}} = state().
t_reregister({_, [LPid | _]}) ->
?assertEqual(ok, reg(LPid, [dbx, dby])),
?assertEqual(ok, reg(LPid, [dbz, dbw])),
#st{by_pid = ByPid, by_dbname = ByName} = state(),
?assertEqual(1, map_size(ByPid)),
?assertMatch(#{LPid := {_, [dbz, dbw]}}, ByPid),
?assertMatch(
#{
dbz := #{LPid := nil},
dbw := #{LPid := nil}
},
ByName
).
t_invalid_gen_server_messages(_) ->
meck:reset(couch_log),
whereis(?MODULE) ! random_msg,
gen_server:cast(?MODULE, bad_cast),
?assertEqual(ignored, gen_server:call(?MODULE, bad_call)),
?assertEqual(3, meck:num_calls(couch_log, notice, 2)).
-record(lst, {
events = [],
wait_cnt = 0,
wait_pid = undefined
}).
kill_sync(Pid) ->
unlink(Pid),
Ref = monitor(process, Pid),
exit(Pid, kill),
receive
{'DOWN', Ref, _, _, _} -> ok
end.
reg(Pid, Dbs) ->
gen_server:call(?MODULE, {register, Pid, Dbs}).
unreg(Pid) ->
gen_server:call(?MODULE, {unregister, Pid}).
notify(Db, Event) ->
gen_server:cast(?MODULE, {notify, Db, Event}).
state() ->
sys:get_state(?MODULE).
% Listeners receive and save their notification events. {get_events, MinNum,
% self()} call will block until the listener had received at least MinNum
% events. We use this since event notification is asynchronous.
start_listener() ->
spawn(fun() -> loop(#lst{}) end).
loop(#lst{} = St0) ->
St = #lst{events = Events, wait_pid = WaitPid} = respond(St0),
receive
{'$couch_event', Db, Ev} ->
loop(St#lst{events = [{Db, Ev} | Events]});
{get_events, _, WPid} when is_pid(WaitPid) ->
WPid ! {error, existing_waiter};
{get_events, WCnt, WPid} ->
loop(St#lst{wait_cnt = WCnt, wait_pid = WPid})
end.
respond(#lst{events = Events, wait_cnt = Cnt, wait_pid = Pid} = St) ->
case {length(Events) >= Cnt, is_pid(Pid)} of
{true, true} ->
Pid ! Events,
St#lst{events = [], wait_cnt = 0, wait_pid = undefined};
{_, _} ->
St
end.
wait(LPid, N) ->
LPid ! {get_events, N, self()},
receive
{error, Error} -> {error, Error};
Events when is_list(Events) -> Events
end.
-endif.