From d02f6cf7a4857ba902884c7d3e29ae8ac65cb698 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Fri, 30 Apr 2010 21:10:06 -0400 Subject: skeleton for new RPC server --- ebin/rexi.app | 9 +++++++++ src/rexi_app.erl | 11 +++++++++++ src/rexi_server.erl | 33 +++++++++++++++++++++++++++++++++ src/rexi_sup.erl | 15 +++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 ebin/rexi.app create mode 100644 src/rexi_app.erl create mode 100644 src/rexi_server.erl create mode 100644 src/rexi_sup.erl diff --git a/ebin/rexi.app b/ebin/rexi.app new file mode 100644 index 00000000..bc7350a9 --- /dev/null +++ b/ebin/rexi.app @@ -0,0 +1,9 @@ +{application, rexi, [ + {description, "lightweight RPC server"}, + {vsn, "1.0"}, + {modules, [rexi_app, rexi_sup, rexi_server]}, + {registered, [rexi_sup, rexi_server]}, + {applications, [kernel, stdlib]}, + {mod, {rexi_app,[]}}, + {start_phases, []} +]}. \ No newline at end of file diff --git a/src/rexi_app.erl b/src/rexi_app.erl new file mode 100644 index 00000000..dda57752 --- /dev/null +++ b/src/rexi_app.erl @@ -0,0 +1,11 @@ +-module(rexi_app). +-behaviour(application). +-export([start/2, stop/1]). + +-include_lib("eunit/include/eunit.hrl"). + +start(_Type, StartArgs) -> + rexi_sup:start_link(StartArgs). + +stop(_State) -> + ok. diff --git a/src/rexi_server.erl b/src/rexi_server.erl new file mode 100644 index 00000000..0e443607 --- /dev/null +++ b/src/rexi_server.erl @@ -0,0 +1,33 @@ +-module(rexi_server). +-behaviour(gen_server). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). + +-export([start_link/0]). + +-include_lib("eunit/include/eunit.hrl"). + +-record(st, { + field = value +}). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + {ok, #st{}}. + +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, State, _Extra) -> + {ok, State}. diff --git a/src/rexi_sup.erl b/src/rexi_sup.erl new file mode 100644 index 00000000..3a518e7b --- /dev/null +++ b/src/rexi_sup.erl @@ -0,0 +1,15 @@ +-module(rexi_sup). +-behaviour(supervisor). +-export([init/1]). + +-export([start_link/1]). + +-include_lib("eunit/include/eunit.hrl"). + +start_link(Args) -> + supervisor:start_link({local,?MODULE}, ?MODULE, Args). + +init([]) -> + Mod = rexi_server, + Spec = {Mod, {Mod,start_link,[]}, permanent, 100, worker, [Mod]}, + {ok, {{one_for_one, 3, 10}, [Spec]}}. -- cgit v1.2.3 From 09a9e6e6e88974b5bcc91a35275b71a104846674 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Fri, 30 Apr 2010 22:51:07 -0400 Subject: better abstraction in rexi_server --- ebin/rexi.app | 2 +- src/rexi_server.erl | 52 +++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/ebin/rexi.app b/ebin/rexi.app index bc7350a9..c705898a 100644 --- a/ebin/rexi.app +++ b/ebin/rexi.app @@ -1,7 +1,7 @@ {application, rexi, [ {description, "lightweight RPC server"}, {vsn, "1.0"}, - {modules, [rexi_app, rexi_sup, rexi_server]}, + {modules, [rexi, rexi_app, rexi_sup, rexi_server]}, {registered, [rexi_sup, rexi_server]}, {applications, [kernel, stdlib]}, {mod, {rexi_app,[]}}, diff --git a/src/rexi_server.erl b/src/rexi_server.erl index 0e443607..65110535 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -3,12 +3,12 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --export([start_link/0]). +-export([start_link/0, init_p/2]). -include_lib("eunit/include/eunit.hrl"). -record(st, { - field = value + workers = [] }). start_link() -> @@ -17,17 +17,47 @@ start_link() -> init([]) -> {ok, #st{}}. -handle_call(_Request, _From, State) -> - {reply, ok, State}. +handle_call(_Request, _From, St) -> + {reply, ok, St}. -handle_cast(_Msg, State) -> - {noreply, State}. +handle_cast({doit, From, MFA}, #st{workers=Workers} = St) -> + {LocalPid, Ref} = spawn_monitor(?MODULE, init_p, [From, MFA]), + {noreply, St#st{workers = add_worker({LocalPid, Ref, From}, Workers)}}. -handle_info(_Info, State) -> - {noreply, State}. +handle_info({'DOWN', Ref, process, _, normal}, #st{workers=Workers} = St) -> + {noreply, St#st{workers = remove_worker(Ref, Workers)}}; -terminate(_Reason, _State) -> +handle_info({'DOWN', Ref, process, Pid, Reason}, #st{workers=Workers} = St) -> + case find_worker(Ref, Workers) of + {Pid, Ref, From} -> + notify_caller(From, Pid, Reason); + false -> ok end, + {noreply, St#st{workers = remove_worker(Ref, Workers)}}; + +handle_info(_Info, St) -> + {noreply, St}. + +terminate(_Reason, St) -> + [exit(Pid,kill) || {Pid, _, _} <- St#st.workers], ok. -code_change(_OldVsn, State, _Extra) -> - {ok, State}. +code_change(_OldVsn, St, _Extra) -> + {ok, St}. + +init_p(From, {M,F,A}) -> + put(rexi_from, From), + try apply(M, F, A) catch _:Reason -> exit(Reason) end. + +%% internal + +add_worker(Worker, List) -> + [Worker | List]. + +remove_worker(Ref, List) -> + lists:keydelete(Ref, 2, List). + +find_worker(Ref, List) -> + lists:keyfind(Ref, 2, List). + +notify_caller({Caller, CallerRef}, Pid, Reason) -> + Caller ! {worker_died, CallerRef, Pid, Reason}. -- cgit v1.2.3 From 6e6813d355aa7371f63e46472faaa2b326ddf394 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Sat, 1 May 2010 08:37:53 -0400 Subject: rexi API module, and change msg sent if rexi worker dies --- src/rexi.erl | 38 ++++++++++++++++++++++++++++++++++++++ src/rexi_server.erl | 8 +++++--- 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/rexi.erl diff --git a/src/rexi.erl b/src/rexi.erl new file mode 100644 index 00000000..745afedb --- /dev/null +++ b/src/rexi.erl @@ -0,0 +1,38 @@ +-module(rexi). +-export([start/0, stop/0, restart/0]). +-export([cast/2, cast/3]). +-export([reply/1]). + +-define(SERVER, rexi_server). + +start() -> + application:start(rexi). + +stop() -> + application:stop(rexi). + +restart() -> + stop(), start(). + +-spec cast(node(), mfa()) -> {ok, reference()}. +cast(Node, MFA) -> + cast(Node, self(), MFA). + +%% @doc Executes apply(M, F, A) on Node. +%% You might want to use this instead of rpc:cast/4 for two reasons. First, +%% the Caller pid and the returned reference are inserted into the remote +%% process' dictionary as 'rexi_from', so it has a way to communicate with you. +%% Second, the remote process is monitored. If it dies, Caller will receive a +%% message of the form `{rexi_EXIT, Ref, Reason}' where Ref is the returned +%% reference and Reason is the exit reason. +-spec cast(node(), pid(), mfa()) -> {ok, reference()}. +cast(Node, Caller, MFA) -> + Ref = make_ref(), + ok = gen_server:cast({?SERVER, Node}, {doit, {Caller,Ref}, MFA}), + {ok, Ref}. + +%% @doc convenience function to reply to the original rexi Caller. +-spec reply(any()) -> any(). +reply(Reply) -> + {Caller, Ref} = get(rexi_from), + erlang:send(Caller, {Ref,Reply}). diff --git a/src/rexi_server.erl b/src/rexi_server.erl index 65110535..e06fd86d 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -30,7 +30,7 @@ handle_info({'DOWN', Ref, process, _, normal}, #st{workers=Workers} = St) -> handle_info({'DOWN', Ref, process, Pid, Reason}, #st{workers=Workers} = St) -> case find_worker(Ref, Workers) of {Pid, Ref, From} -> - notify_caller(From, Pid, Reason); + notify_caller(From, Reason); false -> ok end, {noreply, St#st{workers = remove_worker(Ref, Workers)}}; @@ -44,6 +44,8 @@ terminate(_Reason, St) -> code_change(_OldVsn, St, _Extra) -> {ok, St}. +%% @doc initializes a process started by rexi_server. +-spec init_p({pid(),reference()}, mfa()) -> any(). init_p(From, {M,F,A}) -> put(rexi_from, From), try apply(M, F, A) catch _:Reason -> exit(Reason) end. @@ -59,5 +61,5 @@ remove_worker(Ref, List) -> find_worker(Ref, List) -> lists:keyfind(Ref, 2, List). -notify_caller({Caller, CallerRef}, Pid, Reason) -> - Caller ! {worker_died, CallerRef, Pid, Reason}. +notify_caller({Caller, Ref}, Reason) -> + erlang:send(Caller, {rexi_EXIT, Ref, Reason}). -- cgit v1.2.3 From 513cdd3ac1e23c8bf1f71c6225348f162f193d2d Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Sat, 1 May 2010 10:23:20 -0400 Subject: add ability to kill a remote worker, BugzID 10096 --- src/rexi.erl | 10 ++++++++-- src/rexi_server.erl | 9 ++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/rexi.erl b/src/rexi.erl index 745afedb..2608ee24 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -1,6 +1,6 @@ -module(rexi). -export([start/0, stop/0, restart/0]). --export([cast/2, cast/3]). +-export([cast/2, cast/3, kill/2]). -export([reply/1]). -define(SERVER, rexi_server). @@ -21,7 +21,7 @@ cast(Node, MFA) -> %% @doc Executes apply(M, F, A) on Node. %% You might want to use this instead of rpc:cast/4 for two reasons. First, %% the Caller pid and the returned reference are inserted into the remote -%% process' dictionary as 'rexi_from', so it has a way to communicate with you. +%% process' dictionary as `rexi_from', so it has a way to communicate with you. %% Second, the remote process is monitored. If it dies, Caller will receive a %% message of the form `{rexi_EXIT, Ref, Reason}' where Ref is the returned %% reference and Reason is the exit reason. @@ -31,6 +31,12 @@ cast(Node, Caller, MFA) -> ok = gen_server:cast({?SERVER, Node}, {doit, {Caller,Ref}, MFA}), {ok, Ref}. +%% @doc Sends an async kill signal to the remote process associated with Ref. +%% No rexi_EXIT message will be sent. +-spec kill(node(), reference()) -> ok. +kill(Node, Ref) -> + ok = gen_server:cast({?SERVER, Node}, {kill, Ref}). + %% @doc convenience function to reply to the original rexi Caller. -spec reply(any()) -> any(). reply(Reply) -> diff --git a/src/rexi_server.erl b/src/rexi_server.erl index e06fd86d..6f8b8641 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -22,7 +22,14 @@ handle_call(_Request, _From, St) -> handle_cast({doit, From, MFA}, #st{workers=Workers} = St) -> {LocalPid, Ref} = spawn_monitor(?MODULE, init_p, [From, MFA]), - {noreply, St#st{workers = add_worker({LocalPid, Ref, From}, Workers)}}. + {noreply, St#st{workers = add_worker({LocalPid, Ref, From}, Workers)}}; + +handle_cast({kill, Ref}, #st{workers=Workers} = St) -> + case find_worker(Ref, Workers) of + {Pid, Ref, _} -> + exit(Pid, kill); + false -> ok end, + {noreply, St#st{workers = remove_worker(Ref, Workers)}}. handle_info({'DOWN', Ref, process, _, normal}, #st{workers=Workers} = St) -> {noreply, St#st{workers = remove_worker(Ref, Workers)}}; -- cgit v1.2.3 From 50cef5b4882ebe8f85ca0513f073f3cf6dc68502 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Mon, 10 May 2010 16:06:34 -0400 Subject: add function to make a non-blocking gen_server:call --- src/rexi.erl | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/rexi.erl b/src/rexi.erl index 2608ee24..fd195a26 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -37,8 +37,35 @@ cast(Node, Caller, MFA) -> kill(Node, Ref) -> ok = gen_server:cast({?SERVER, Node}, {kill, Ref}). +%% @equiv async_server_call(Server, self(), Request) +-spec async_server_call(pid() | {atom(),node()}, any()) -> reference(). +async_server_call(Server, Request) -> + async_server_call(Server, self(), Request). + +%% @doc Sends a properly formatted gen_server:call Request to the Server and +%% returns the reference which the Server will include in its reply. The +%% function acts more like cast() than call() in that the server process +%% is not monitored. Clients who want to know if the server is alive should +%% monitor it themselves before calling this function. +-spec async_server_call(pid() | {atom(),node()}, pid(), any()) -> reference(). +async_server_call(Server, Caller, Name, Request) -> + Ref = make_ref(), + do_send(Server, {'$gen_call', {Caller,Ref}, Request}), + Ref. + %% @doc convenience function to reply to the original rexi Caller. -spec reply(any()) -> any(). reply(Reply) -> {Caller, Ref} = get(rexi_from), erlang:send(Caller, {Ref,Reply}). + +%% internal functions %% + +% send a message as quickly as possible +do_send(Dest, Msg) -> + case erlang:send(Dest, Msg, [noconnect]) of + noconnect -> + spawn(erlang, send, [Dest, Msg]); + ok -> + ok + end. -- cgit v1.2.3 From c30010994b78818b758e6bb9da2017984a14fb63 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Mon, 10 May 2010 16:07:24 -0400 Subject: drop ok tag from cast() responses, update docs --- src/rexi.erl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/rexi.erl b/src/rexi.erl index fd195a26..0eaa3249 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -14,7 +14,8 @@ stop() -> restart() -> stop(), start(). --spec cast(node(), mfa()) -> {ok, reference()}. +%% @equiv cast(Node, self(), MFA) +-spec cast(node(), mfa()) -> reference(). cast(Node, MFA) -> cast(Node, self(), MFA). @@ -22,14 +23,14 @@ cast(Node, MFA) -> %% You might want to use this instead of rpc:cast/4 for two reasons. First, %% the Caller pid and the returned reference are inserted into the remote %% process' dictionary as `rexi_from', so it has a way to communicate with you. -%% Second, the remote process is monitored. If it dies, Caller will receive a -%% message of the form `{rexi_EXIT, Ref, Reason}' where Ref is the returned -%% reference and Reason is the exit reason. --spec cast(node(), pid(), mfa()) -> {ok, reference()}. +%% Second, the remote process is monitored. If it exits with a Reason other +%% than normal, Caller will receive a message of the form +%% `{rexi_EXIT, Ref, Reason}' where Ref is the returned reference. +-spec cast(node(), pid(), mfa()) -> reference(). cast(Node, Caller, MFA) -> Ref = make_ref(), ok = gen_server:cast({?SERVER, Node}, {doit, {Caller,Ref}, MFA}), - {ok, Ref}. + Ref. %% @doc Sends an async kill signal to the remote process associated with Ref. %% No rexi_EXIT message will be sent. -- cgit v1.2.3 From 6b1b19e24eb77b67a52fed0a9f8237efd8d7847b Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Mon, 10 May 2010 16:28:13 -0400 Subject: oops, sloppy initial commit of non-blocking call --- src/rexi.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rexi.erl b/src/rexi.erl index 0eaa3249..abcea295 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -2,6 +2,7 @@ -export([start/0, stop/0, restart/0]). -export([cast/2, cast/3, kill/2]). -export([reply/1]). +-export([async_server_call/2, async_server_call/3]). -define(SERVER, rexi_server). @@ -49,7 +50,7 @@ async_server_call(Server, Request) -> %% is not monitored. Clients who want to know if the server is alive should %% monitor it themselves before calling this function. -spec async_server_call(pid() | {atom(),node()}, pid(), any()) -> reference(). -async_server_call(Server, Caller, Name, Request) -> +async_server_call(Server, Caller, Request) -> Ref = make_ref(), do_send(Server, {'$gen_call', {Caller,Ref}, Request}), Ref. -- cgit v1.2.3 From cadb7ffad91c9a79f454fc215fa135ceef0910c5 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Mon, 10 May 2010 16:06:34 -0400 Subject: add function to make a non-blocking gen_server:call (cherry picked from commit 52ab0e4418e0e3cfeb848367f05813d312a6085e) --- src/rexi.erl | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/rexi.erl b/src/rexi.erl index 2608ee24..fd195a26 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -37,8 +37,35 @@ cast(Node, Caller, MFA) -> kill(Node, Ref) -> ok = gen_server:cast({?SERVER, Node}, {kill, Ref}). +%% @equiv async_server_call(Server, self(), Request) +-spec async_server_call(pid() | {atom(),node()}, any()) -> reference(). +async_server_call(Server, Request) -> + async_server_call(Server, self(), Request). + +%% @doc Sends a properly formatted gen_server:call Request to the Server and +%% returns the reference which the Server will include in its reply. The +%% function acts more like cast() than call() in that the server process +%% is not monitored. Clients who want to know if the server is alive should +%% monitor it themselves before calling this function. +-spec async_server_call(pid() | {atom(),node()}, pid(), any()) -> reference(). +async_server_call(Server, Caller, Name, Request) -> + Ref = make_ref(), + do_send(Server, {'$gen_call', {Caller,Ref}, Request}), + Ref. + %% @doc convenience function to reply to the original rexi Caller. -spec reply(any()) -> any(). reply(Reply) -> {Caller, Ref} = get(rexi_from), erlang:send(Caller, {Ref,Reply}). + +%% internal functions %% + +% send a message as quickly as possible +do_send(Dest, Msg) -> + case erlang:send(Dest, Msg, [noconnect]) of + noconnect -> + spawn(erlang, send, [Dest, Msg]); + ok -> + ok + end. -- cgit v1.2.3 From ca12b1656e39afd117880526a34e5320fb3bc976 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Mon, 10 May 2010 16:07:24 -0400 Subject: drop ok tag from cast() responses, update docs (cherry picked from commit 23bc964bdd8b89be9af53cdd4da53603fe2edb3f) --- src/rexi.erl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/rexi.erl b/src/rexi.erl index fd195a26..0eaa3249 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -14,7 +14,8 @@ stop() -> restart() -> stop(), start(). --spec cast(node(), mfa()) -> {ok, reference()}. +%% @equiv cast(Node, self(), MFA) +-spec cast(node(), mfa()) -> reference(). cast(Node, MFA) -> cast(Node, self(), MFA). @@ -22,14 +23,14 @@ cast(Node, MFA) -> %% You might want to use this instead of rpc:cast/4 for two reasons. First, %% the Caller pid and the returned reference are inserted into the remote %% process' dictionary as `rexi_from', so it has a way to communicate with you. -%% Second, the remote process is monitored. If it dies, Caller will receive a -%% message of the form `{rexi_EXIT, Ref, Reason}' where Ref is the returned -%% reference and Reason is the exit reason. --spec cast(node(), pid(), mfa()) -> {ok, reference()}. +%% Second, the remote process is monitored. If it exits with a Reason other +%% than normal, Caller will receive a message of the form +%% `{rexi_EXIT, Ref, Reason}' where Ref is the returned reference. +-spec cast(node(), pid(), mfa()) -> reference(). cast(Node, Caller, MFA) -> Ref = make_ref(), ok = gen_server:cast({?SERVER, Node}, {doit, {Caller,Ref}, MFA}), - {ok, Ref}. + Ref. %% @doc Sends an async kill signal to the remote process associated with Ref. %% No rexi_EXIT message will be sent. -- cgit v1.2.3 From a7bdc595544d23457b2ffdea4514d6a9ef4634eb Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Mon, 10 May 2010 16:28:13 -0400 Subject: oops, sloppy initial commit of non-blocking call (cherry picked from commit 34a36f3414615524d737fc38140eec660bbcd81f) --- src/rexi.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rexi.erl b/src/rexi.erl index 0eaa3249..abcea295 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -2,6 +2,7 @@ -export([start/0, stop/0, restart/0]). -export([cast/2, cast/3, kill/2]). -export([reply/1]). +-export([async_server_call/2, async_server_call/3]). -define(SERVER, rexi_server). @@ -49,7 +50,7 @@ async_server_call(Server, Request) -> %% is not monitored. Clients who want to know if the server is alive should %% monitor it themselves before calling this function. -spec async_server_call(pid() | {atom(),node()}, pid(), any()) -> reference(). -async_server_call(Server, Caller, Name, Request) -> +async_server_call(Server, Caller, Request) -> Ref = make_ref(), do_send(Server, {'$gen_call', {Caller,Ref}, Request}), Ref. -- cgit v1.2.3 From 6214c19346095b775ecf9c7d007fedba38aabd47 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Mon, 10 May 2010 20:16:22 -0400 Subject: code for efficient monitoring of many remote processes. BugzID 10096 --- ebin/rexi.app | 2 +- src/rexi_monitor.erl | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/rexi_monitor.erl diff --git a/ebin/rexi.app b/ebin/rexi.app index c705898a..e8145c6b 100644 --- a/ebin/rexi.app +++ b/ebin/rexi.app @@ -1,7 +1,7 @@ {application, rexi, [ {description, "lightweight RPC server"}, {vsn, "1.0"}, - {modules, [rexi, rexi_app, rexi_sup, rexi_server]}, + {modules, [rexi, rexi_app, rexi_sup, rexi_monitor, rexi_server]}, {registered, [rexi_sup, rexi_server]}, {applications, [kernel, stdlib]}, {mod, {rexi_app,[]}}, diff --git a/src/rexi_monitor.erl b/src/rexi_monitor.erl new file mode 100644 index 00000000..b4c00f23 --- /dev/null +++ b/src/rexi_monitor.erl @@ -0,0 +1,39 @@ +-module(rexi_monitor). +-export([start/1, stop/1]). + +-include_lib("eunit/include/eunit.hrl"). + +%% @doc spawn_links a process which monitors the supplied list of items and +%% returns the process ID. +-spec start([pid() | atom() | {atom(),atom()}]) -> pid(). +start(Procs) -> + Parent = self(), + spawn_link(fun() -> + [erlang:monitor(process, P) || P <- Procs], + wait_monitors(Parent) + end). + +%% @doc Cleanly shut down the monitoring process and flush all rexi_DOWN +%% messages from our mailbox. +-spec stop(pid()) -> ok. +stop(MonitoringPid) -> + MonitoringPid ! {self(), shutdown}, + flush_down_messages(). + +%% internal functions %% + +wait_monitors(Parent) -> + receive + {'DOWN', _, process, Pid, Reason} -> + Parent ! {rexi_DOWN, self(), Pid, Reason}, + wait_monitors(Parent); + {Parent, shutdown} -> + ok + end. + +flush_down_messages() -> + receive {rexi_DOWN, _, _, _} -> + flush_down_messages() + after 0 -> + ok + end. -- cgit v1.2.3 From 04de7e4c03eaae39d914fd2c88e99300dbf53cd9 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Mon, 10 May 2010 21:05:28 -0400 Subject: never block in notify_caller, BugzID 10096 --- src/rexi_server.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rexi_server.erl b/src/rexi_server.erl index 6f8b8641..1589ba3b 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -69,4 +69,4 @@ find_worker(Ref, List) -> lists:keyfind(Ref, 2, List). notify_caller({Caller, Ref}, Reason) -> - erlang:send(Caller, {rexi_EXIT, Ref, Reason}). + spawn(erlang, send, [Caller, {rexi_EXIT, Ref, Reason}]). -- cgit v1.2.3 From 1819b3aff71f49087e98c671e3ce3a388be98543 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Mon, 10 May 2010 21:14:25 -0400 Subject: Better docs for rexi_monitor --- src/rexi_monitor.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rexi_monitor.erl b/src/rexi_monitor.erl index b4c00f23..7e16048c 100644 --- a/src/rexi_monitor.erl +++ b/src/rexi_monitor.erl @@ -4,7 +4,8 @@ -include_lib("eunit/include/eunit.hrl"). %% @doc spawn_links a process which monitors the supplied list of items and -%% returns the process ID. +%% returns the process ID. If a monitored process exits, the caller will +%% receive a {rexi_DOWN, MonitoringPid, DeadPid, Reason} message. -spec start([pid() | atom() | {atom(),atom()}]) -> pid(). start(Procs) -> Parent = self(), -- cgit v1.2.3 From 60ce78e4cab5d3f379c50e9a51c4d1dba71b1b1c Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Tue, 11 May 2010 10:42:12 -0400 Subject: formatting changes for rexi_server --- src/rexi_server.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rexi_server.erl b/src/rexi_server.erl index 6f8b8641..10bc90d3 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -1,6 +1,6 @@ -module(rexi_server). -behaviour(gen_server). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([start_link/0, init_p/2]). @@ -18,7 +18,7 @@ init([]) -> {ok, #st{}}. handle_call(_Request, _From, St) -> - {reply, ok, St}. + {reply, ignored, St}. handle_cast({doit, From, MFA}, #st{workers=Workers} = St) -> {LocalPid, Ref} = spawn_monitor(?MODULE, init_p, [From, MFA]), -- cgit v1.2.3 From 28ff0e1c5c49a25771cd59669fe13d2372a07716 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Mon, 17 May 2010 12:57:45 -0400 Subject: slightly better spec string --- src/rexi_monitor.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rexi_monitor.erl b/src/rexi_monitor.erl index 7e16048c..bbff22b3 100644 --- a/src/rexi_monitor.erl +++ b/src/rexi_monitor.erl @@ -6,7 +6,7 @@ %% @doc spawn_links a process which monitors the supplied list of items and %% returns the process ID. If a monitored process exits, the caller will %% receive a {rexi_DOWN, MonitoringPid, DeadPid, Reason} message. --spec start([pid() | atom() | {atom(),atom()}]) -> pid(). +-spec start([pid() | atom() | {atom(),node()}]) -> pid(). start(Procs) -> Parent = self(), spawn_link(fun() -> -- cgit v1.2.3 From 59dfef1c9a5533601ca2693378de3840ef883ddb Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Wed, 19 May 2010 14:03:46 -0400 Subject: appups for 1.1.2 -> 1.1.3 --- ebin/rexi.appup | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 ebin/rexi.appup diff --git a/ebin/rexi.appup b/ebin/rexi.appup new file mode 100644 index 00000000..7ed8ad73 --- /dev/null +++ b/ebin/rexi.appup @@ -0,0 +1,5 @@ +{"1.1",[{"1.0",[ + {load_module, rexi}, + {add_module, rexi_monitor}, + {load_module, rexi_server} +]}],[{"1.0",[]}]}. -- cgit v1.2.3 From 1ebf9657e9fd9e2e34435e8d90b92b685be3f3c9 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Thu, 20 May 2010 11:36:01 -0400 Subject: app and appup version bumps to 1.1.4 --- ebin/rexi.app | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebin/rexi.app b/ebin/rexi.app index e8145c6b..b00cc4f6 100644 --- a/ebin/rexi.app +++ b/ebin/rexi.app @@ -1,6 +1,6 @@ {application, rexi, [ {description, "lightweight RPC server"}, - {vsn, "1.0"}, + {vsn, "1.1"}, {modules, [rexi, rexi_app, rexi_sup, rexi_monitor, rexi_server]}, {registered, [rexi_sup, rexi_server]}, {applications, [kernel, stdlib]}, -- cgit v1.2.3 From 03451da61ceb390805861e6bb50aea3904d4ff28 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Tue, 25 May 2010 12:42:51 -0400 Subject: _changes resource for feed=normal, BugzID 1330 --- src/rexi.erl | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/rexi.erl b/src/rexi.erl index abcea295..a344af7d 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -1,7 +1,7 @@ -module(rexi). -export([start/0, stop/0, restart/0]). -export([cast/2, cast/3, kill/2]). --export([reply/1]). +-export([reply/1, sync_reply/1, sync_reply/2]). -export([async_server_call/2, async_server_call/3]). -define(SERVER, rexi_server). @@ -61,6 +61,24 @@ reply(Reply) -> {Caller, Ref} = get(rexi_from), erlang:send(Caller, {Ref,Reply}). +%% @equiv sync_reply(Reply, infinity) +sync_reply(Reply) -> + sync_reply(Reply, infinity). + +%% @doc convenience function to reply to caller and wait for response. Message +%% is of the form {OriginalRef, {self(),reference()}, Reply}, which enables the +%% original caller to respond back. +-spec sync_reply(any(), pos_integer() | infinity) -> any(). +sync_reply(Reply, Timeout) -> + {Caller, Ref} = get(rexi_from), + Tag = make_ref(), + erlang:send(Caller, {Ref, {self(),Tag}, Reply}), + receive {Tag, Response} -> + Response + after Timeout -> + timeout + end. + %% internal functions %% % send a message as quickly as possible -- cgit v1.2.3 From f496b4ab3e6dfec96792edd7e9fc6836fb7c2b36 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Thu, 27 May 2010 09:38:13 -0400 Subject: new format of rexi_EXIT message --- src/rexi_server.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rexi_server.erl b/src/rexi_server.erl index 10bc90d3..8b922275 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -69,4 +69,4 @@ find_worker(Ref, List) -> lists:keyfind(Ref, 2, List). notify_caller({Caller, Ref}, Reason) -> - erlang:send(Caller, {rexi_EXIT, Ref, Reason}). + Caller ! {Ref, {rexi_EXIT, Reason}}. -- cgit v1.2.3 From a2843a05ac55ff639a7f622e02a850e1d74947f8 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Thu, 27 May 2010 15:42:21 -0400 Subject: updated docs for format of error message --- src/rexi.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rexi.erl b/src/rexi.erl index abcea295..fced6ab2 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -26,7 +26,7 @@ cast(Node, MFA) -> %% process' dictionary as `rexi_from', so it has a way to communicate with you. %% Second, the remote process is monitored. If it exits with a Reason other %% than normal, Caller will receive a message of the form -%% `{rexi_EXIT, Ref, Reason}' where Ref is the returned reference. +%% `{Ref, {rexi_EXIT, Reason}}' where Ref is the returned reference. -spec cast(node(), pid(), mfa()) -> reference(). cast(Node, Caller, MFA) -> Ref = make_ref(), -- cgit v1.2.3 From 850e7c23551696c952b8f1a57b76efa38fcf6f77 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Tue, 1 Jun 2010 10:58:17 -0400 Subject: add sync_reply, change msg format to be more like gen_server --- src/rexi.erl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/rexi.erl b/src/rexi.erl index fced6ab2..aca029b5 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -61,6 +61,24 @@ reply(Reply) -> {Caller, Ref} = get(rexi_from), erlang:send(Caller, {Ref,Reply}). +%% @equiv sync_reply(Reply, infinity) +sync_reply(Reply) -> + sync_reply(Reply, infinity). + +%% @doc convenience function to reply to caller and wait for response. Message +%% is of the form {OriginalRef, {self(),reference()}, Reply}, which enables the +%% original caller to respond back. +-spec sync_reply(any(), pos_integer() | infinity) -> any(). +sync_reply(Reply, Timeout) -> + {Caller, Ref} = get(rexi_from), + Tag = make_ref(), + erlang:send(Caller, {Ref, {self(),Tag}, Reply}), + receive {Tag, Response} -> + Response + after Timeout -> + timeout + end. + %% internal functions %% % send a message as quickly as possible -- cgit v1.2.3 From 3194065e8abdc532a015520312a45a0a4bc48f6c Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Tue, 1 Jun 2010 23:03:56 -0400 Subject: all_docs resource w/o keylist, BugzID 10218 --- src/rexi.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rexi.erl b/src/rexi.erl index aca029b5..8f1d72bf 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -1,7 +1,7 @@ -module(rexi). -export([start/0, stop/0, restart/0]). -export([cast/2, cast/3, kill/2]). --export([reply/1]). +-export([reply/1, sync_reply/1, sync_reply/2]). -export([async_server_call/2, async_server_call/3]). -define(SERVER, rexi_server). -- cgit v1.2.3 From 32047971353f8a7a9e27d874c49deaa51f22d8de Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Wed, 9 Jun 2010 17:07:05 -0400 Subject: updates to .app resource files --- ebin/rexi.app | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ebin/rexi.app b/ebin/rexi.app index c705898a..e54a2533 100644 --- a/ebin/rexi.app +++ b/ebin/rexi.app @@ -1,9 +1,8 @@ {application, rexi, [ - {description, "lightweight RPC server"}, + {description, "Lightweight RPC server"}, {vsn, "1.0"}, {modules, [rexi, rexi_app, rexi_sup, rexi_server]}, {registered, [rexi_sup, rexi_server]}, {applications, [kernel, stdlib]}, - {mod, {rexi_app,[]}}, - {start_phases, []} + {mod, {rexi_app,[]}} ]}. \ No newline at end of file -- cgit v1.2.3 From 9cd898d5bb0436182b6642972e90c94460a8eb61 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Fri, 11 Jun 2010 14:57:41 -0400 Subject: update rexi.app to 1.2 to match dbcore 1.1.x numbering --- ebin/rexi.app | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ebin/rexi.app b/ebin/rexi.app index 6e80386a..620c8863 100644 --- a/ebin/rexi.app +++ b/ebin/rexi.app @@ -1,8 +1,8 @@ {application, rexi, [ - {description, "lightweight RPC server"}, - {vsn, "1.1"}, + {description, "Lightweight RPC server"}, + {vsn, "1.2"}, {modules, [rexi, rexi_app, rexi_sup, rexi_monitor, rexi_server]}, {registered, [rexi_sup, rexi_server]}, {applications, [kernel, stdlib]}, {mod, {rexi_app,[]}} -]}. \ No newline at end of file +]}. -- cgit v1.2.3 From a3191c5438440dced103c4c9677e8859b71121dc Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Thu, 17 Jun 2010 10:17:40 -0400 Subject: switch to ets for managing the workers --- src/rexi_server.erl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/rexi_server.erl b/src/rexi_server.erl index 8b922275..f45ca4fa 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -8,7 +8,7 @@ -include_lib("eunit/include/eunit.hrl"). -record(st, { - workers = [] + workers = ets:new(workers, [private, {keypos,2}]) }). start_link() -> @@ -45,7 +45,7 @@ handle_info(_Info, St) -> {noreply, St}. terminate(_Reason, St) -> - [exit(Pid,kill) || {Pid, _, _} <- St#st.workers], + ets:foldl(fun({Pid, _, _}, _) -> exit(Pid,kill) end, nil, St#st.workers), ok. code_change(_OldVsn, St, _Extra) -> @@ -59,14 +59,14 @@ init_p(From, {M,F,A}) -> %% internal -add_worker(Worker, List) -> - [Worker | List]. +add_worker(Worker, Tab) -> + ets:insert(Tab, Worker), Tab. -remove_worker(Ref, List) -> - lists:keydelete(Ref, 2, List). +remove_worker(Ref, Tab) -> + ets:delete(Tab, Ref), Tab. -find_worker(Ref, List) -> - lists:keyfind(Ref, 2, List). +find_worker(Ref, Tab) -> + case ets:lookup(Tab, Ref) of [] -> false; [Worker] -> Worker end. notify_caller({Caller, Ref}, Reason) -> Caller ! {Ref, {rexi_EXIT, Reason}}. -- cgit v1.2.3 From 6c1079a922a4d96d6dca101e1a7d68a93f5b8c7c Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Thu, 17 Jun 2010 10:18:29 -0400 Subject: demonitor before killing the worker --- src/rexi_server.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rexi_server.erl b/src/rexi_server.erl index f45ca4fa..127e088c 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -27,6 +27,7 @@ handle_cast({doit, From, MFA}, #st{workers=Workers} = St) -> handle_cast({kill, Ref}, #st{workers=Workers} = St) -> case find_worker(Ref, Workers) of {Pid, Ref, _} -> + erlang:demonitor(Ref), exit(Pid, kill); false -> ok end, {noreply, St#st{workers = remove_worker(Ref, Workers)}}. -- cgit v1.2.3 From 69d30df9ac93976743c3decf56c471508cb6f897 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Sat, 10 Jul 2010 15:50:17 -0400 Subject: thank you dialyzer --- src/rexi.erl | 4 ++-- src/rexi_server.erl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rexi.erl b/src/rexi.erl index 8f1d72bf..21250210 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -16,7 +16,7 @@ restart() -> stop(), start(). %% @equiv cast(Node, self(), MFA) --spec cast(node(), mfa()) -> reference(). +-spec cast(node(), {atom(), atom(), list()}) -> reference(). cast(Node, MFA) -> cast(Node, self(), MFA). @@ -27,7 +27,7 @@ cast(Node, MFA) -> %% Second, the remote process is monitored. If it exits with a Reason other %% than normal, Caller will receive a message of the form %% `{Ref, {rexi_EXIT, Reason}}' where Ref is the returned reference. --spec cast(node(), pid(), mfa()) -> reference(). +-spec cast(node(), pid(), {atom(), atom(), list()}) -> reference(). cast(Node, Caller, MFA) -> Ref = make_ref(), ok = gen_server:cast({?SERVER, Node}, {doit, {Caller,Ref}, MFA}), diff --git a/src/rexi_server.erl b/src/rexi_server.erl index 127e088c..70296d5b 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -53,7 +53,7 @@ code_change(_OldVsn, St, _Extra) -> {ok, St}. %% @doc initializes a process started by rexi_server. --spec init_p({pid(),reference()}, mfa()) -> any(). +-spec init_p({pid(), reference()}, {atom(), atom(), list()}) -> any(). init_p(From, {M,F,A}) -> put(rexi_from, From), try apply(M, F, A) catch _:Reason -> exit(Reason) end. -- cgit v1.2.3 From 13a8075c07f148626049ac780f472898c4e2b76c Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Wed, 14 Jul 2010 16:32:29 -0400 Subject: 5 minute default timeout for sync_reply --- src/rexi.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rexi.erl b/src/rexi.erl index 21250210..8ab1f05e 100644 --- a/src/rexi.erl +++ b/src/rexi.erl @@ -61,9 +61,9 @@ reply(Reply) -> {Caller, Ref} = get(rexi_from), erlang:send(Caller, {Ref,Reply}). -%% @equiv sync_reply(Reply, infinity) +%% @equiv sync_reply(Reply, 300000) sync_reply(Reply) -> - sync_reply(Reply, infinity). + sync_reply(Reply, 300000). %% @doc convenience function to reply to caller and wait for response. Message %% is of the form {OriginalRef, {self(),reference()}, Reply}, which enables the -- cgit v1.2.3 From 234fcf93643671318e46fecddf793b2f119418f5 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Wed, 14 Jul 2010 21:13:48 -0400 Subject: kill was looking up the wrong ref(), so it never found anything --- src/rexi_server.erl | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/rexi_server.erl b/src/rexi_server.erl index 70296d5b..5fc342bc 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -24,13 +24,15 @@ handle_cast({doit, From, MFA}, #st{workers=Workers} = St) -> {LocalPid, Ref} = spawn_monitor(?MODULE, init_p, [From, MFA]), {noreply, St#st{workers = add_worker({LocalPid, Ref, From}, Workers)}}; -handle_cast({kill, Ref}, #st{workers=Workers} = St) -> - case find_worker(Ref, Workers) of - {Pid, Ref, _} -> +handle_cast({kill, FromRef}, #st{workers=Workers} = St) -> + case find_worker_from(FromRef, Workers) of + {Pid, KeyRef, {_, FromRef}} -> erlang:demonitor(Ref), - exit(Pid, kill); - false -> ok end, - {noreply, St#st{workers = remove_worker(Ref, Workers)}}. + exit(Pid, kill), + {noreply, St#st{workers = remove_worker(KeyRef, Workers)}}; + false -> + {noreply, St} + end. handle_info({'DOWN', Ref, process, _, normal}, #st{workers=Workers} = St) -> {noreply, St#st{workers = remove_worker(Ref, Workers)}}; @@ -38,9 +40,11 @@ handle_info({'DOWN', Ref, process, _, normal}, #st{workers=Workers} = St) -> handle_info({'DOWN', Ref, process, Pid, Reason}, #st{workers=Workers} = St) -> case find_worker(Ref, Workers) of {Pid, Ref, From} -> - notify_caller(From, Reason); - false -> ok end, - {noreply, St#st{workers = remove_worker(Ref, Workers)}}; + notify_caller(From, Reason), + {noreply, St#st{workers = remove_worker(Ref, Workers)}}; + false -> + {noreply, St} + end; handle_info(_Info, St) -> {noreply, St}. @@ -56,6 +60,7 @@ code_change(_OldVsn, St, _Extra) -> -spec init_p({pid(), reference()}, {atom(), atom(), list()}) -> any(). init_p(From, {M,F,A}) -> put(rexi_from, From), + put(initial_call, {M,F,length(A)}), try apply(M, F, A) catch _:Reason -> exit(Reason) end. %% internal @@ -69,5 +74,13 @@ remove_worker(Ref, Tab) -> find_worker(Ref, Tab) -> case ets:lookup(Tab, Ref) of [] -> false; [Worker] -> Worker end. +find_worker_from(Ref, Tab) -> + case ets:match_object(Tab, {'_', '_', {'_', Ref}}) of + [] -> + false; + [Worker] -> + Worker + end. + notify_caller({Caller, Ref}, Reason) -> Caller ! {Ref, {rexi_EXIT, Reason}}. -- cgit v1.2.3 From 85ae6ca690edad17ceb92b856623a0a599924c77 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Wed, 14 Jul 2010 21:18:57 -0400 Subject: bah, missed a rename --- src/rexi_server.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rexi_server.erl b/src/rexi_server.erl index 5fc342bc..c4dc740d 100644 --- a/src/rexi_server.erl +++ b/src/rexi_server.erl @@ -27,7 +27,7 @@ handle_cast({doit, From, MFA}, #st{workers=Workers} = St) -> handle_cast({kill, FromRef}, #st{workers=Workers} = St) -> case find_worker_from(FromRef, Workers) of {Pid, KeyRef, {_, FromRef}} -> - erlang:demonitor(Ref), + erlang:demonitor(KeyRef), exit(Pid, kill), {noreply, St#st{workers = remove_worker(KeyRef, Workers)}}; false -> -- cgit v1.2.3