Skip to content
This repository has been archived by the owner on Nov 2, 2021. It is now read-only.

Application start synchronization

Ulf Wiger edited this page Sep 10, 2016 · 1 revision

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. In rvi_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.

Clone this wiki locally