-
Notifications
You must be signed in to change notification settings - Fork 35
Application start synchronization
Since the rvi_core
is component-based, each component is realized as an Erlang/OTP application, and some components may be optional, I needed a mechanism to control start dependencies. I use my own utilities setup
and gproc
, already present in rvi_core, to achieve this.
Mechanism:
- The application
rvi
will start last, and wait for all active RVI components to announce that they are running. - The process
rvi_server
keeps track of dependencies and announcements. Inrvi_app.erl
(the application callback), the final wait instruction looks like this:
start_phase(announce, _, _) ->
rvi_server:await();
For straightforward dependencies (e.g. against rvi_common
), there is no need to complicate things; just add rvi_common
to the applications
list in the app file. For now, it's mainly the datalink components that register and announce their presence.
Example: dlink_tls
:
- In the app file (
dlink_tls.app.src
), the following entries bear significance:
{start_phases, [{json_rpc, []}, {connection_manager, []}, {announce, []}]},
{env, [
{rvi_core_await, [{n,l,dlink_tls}]}
]}
]}.
That is, end the start sequence with an announce
start phase (see below), and also define an environment variable, {rvi_core_await, [{n,l,AppName}]}
.
Explicitly revealing the gproc
key structure here is not "abstraction leaking", but rather communicating to the reader that the gproc
API is used to advertise dependencies. This is in line with the reasoning in the 2007 SIGPLAN paper on gproc, i.e. if this practice is followed consistently, gproc becomes a very powerful system inspection and debugging tool.
(One could argue that the use of an rvi_common wrapper in the start phase implementation is rather "abstraction hiding", since all the function does is call gproc:reg(Name)
.)
start_phase(announce, _, _) ->
rvi_common:announce({n, l, dlink_tls}).
So what does (the gen_server) rvi_server
do? On startup, it queries the environment via setup
:
init([]) ->
WaitFor = lists:flatmap(
fun({_, Names}) ->
Names
end, setup:find_env_vars(rvi_core_await)),
{ok, start_timer(#st{wait_for = WaitFor})}.
The function setup:find_env_vars(rvi_core_await)
searches all application environments for instances of the variable rvi_core_await
. This results in a list of gproc
names to wait for. The actual wait is triggered when the call await
comes in from rvi
:
handle_call(await, _From, #st{wait_for = WF} = S) ->
[gproc:nb_wait(Name) || Name <- WF],
{reply, ok, S};
The gproc:nb_wait/1
function will cause a message to be sent to the caller as soon as the name appears. It is then removed from the wait_for
list in rvi_server
, and when that list becomes empty, rvi_core
is announced as started.
Applications that want to wait for rvi_core
to be started, can use rvi_server:ensure_ready(rvi_core, Timeout)
, or simply gproc:await({n, l, rvi_core})
. But since rvi
calls rvi_server:await()
synchronously during its startup, a simple application dependency on rvi
should also suffice.