Packages

Riak Core Lite Benchmarking Framework

Current section

Files

Jump to
rcl_bench src rcl_bench_worker.erl
Raw

src/rcl_bench_worker.erl

-module(rcl_bench_worker).
-behaviour(gen_server).
%% API
-export([start_link/3, run/1, stop/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-record(state,
{id,
keygen,
valgen,
driver_state,
shutdown_on_error,
ops,
ops_len,
parent_pid,
worker_pid,
sup_id,
driver_mod}).
%% ====================================================================
%% API
%% ====================================================================
start_link(SupChild, Id, DriverMod) ->
gen_server:start_link(?MODULE, [SupChild, Id, DriverMod], []).
run(Pids) ->
[ok = gen_server:call(Pid, run) || Pid <- Pids],
ok.
stop(Pids) ->
[ok = gen_server:call(Pid, stop) || Pid <- Pids],
ok.
%% ====================================================================
%% gen_server callbacks
%% ====================================================================
init([SupChild, Id, DriverMod]) ->
%% Setup RNG seed for worker sub-process to use; incorporate the ID of
%% the worker to ensure consistency in load-gen
%%
%% NOTE: If the worker process dies, this obviously introduces some entropy
%% into the equation since you'd be restarting the RNG all over.
%%
%% The RNG_SEED is static by default for replicability of key size
%% and value size generation between test runs.
process_flag(trap_exit, true),
Ops = ops_tuple(DriverMod),
ShutdownOnError = DriverMod:shutdown_on_error(),
%% Finally, initialize key and value generation. We pass in our ID to the
%% initialization to enable (optional) key/value space partitioning
{ok, KeyGenerator} = DriverMod:key_generator(),
{ok, ValueGenerator} = DriverMod:value_generator(),
KeyGenFun = rcl_bench_keygen:new(KeyGenerator, Id),
ValGenFun = rcl_bench_valgen:new(ValueGenerator, Id),
State =
#state{id = Id,
keygen = KeyGenFun,
valgen = ValGenFun,
shutdown_on_error = ShutdownOnError,
ops = Ops,
ops_len = size(Ops),
parent_pid = self(),
sup_id = SupChild,
driver_mod = DriverMod},
%% Use a dedicated sub-process to do the actual work. The work loop may need
%% to sleep or otherwise delay in a way that would be inappropriate and/or
%% inefficient for a gen_server. Furthermore, we want the loop to be as
%% tight as possible for peak load generation and avoid unnecessary polling
%% of the message queue.
%%
%% Link the worker and the sub-process to ensure that if either exits, the
%% other goes with it.
WorkerPid = spawn_link(fun () -> worker_init(State) end),
WorkerPid ! {init_driver, self()},
receive
driver_ready ->
ok
end,
%% start
gen_server:cast(self(), run),
{ok, State#state{worker_pid = WorkerPid}}.
handle_call(run, _From, State) ->
State#state.worker_pid ! run,
{reply, ok, State}.
handle_cast(run, State) ->
State#state.worker_pid ! run,
{noreply, State}.
handle_info({'EXIT', Pid, Reason}, State) ->
case Reason of
normal ->
%% Clean shutdown of the worker; spawn a process to terminate this
%% process via the supervisor API and make sure it doesn't restart.
spawn(fun () ->
stop_worker(State#state.sup_id)
end),
{noreply, State};
_ ->
logger:error("Worker ~p exited with ~p", [Pid, Reason]),
%% Worker process exited for some other reason; stop this process
%% as well so that everything gets restarted by the sup
{stop, normal, State}
end.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% ====================================================================
%% Internal functions
%% ====================================================================
%%
%% Stop a worker process via the supervisor and terminate the app
%% if there are no workers remaining
%%
%% WARNING: Must run from a process other than the worker!
%%
stop_worker(SupChild) ->
ok = rcl_bench_workers_sup:stop_child(SupChild),
case rcl_bench_workers_sup:workers() of
[] ->
%% No more workers -- stop the system
rcl_bench_util:exit("Stopping the system", []);
_ ->
ok
end.
%%
%% Expand operations list into tuple suitable for weighted, random draw
%%
ops_tuple(DriverMod) ->
{ok, Operations} = DriverMod:operations(),
F =
fun ({OpTag, Count}) ->
lists:duplicate(Count, {OpTag, OpTag});
({Label, OpTag, Count}) ->
lists:duplicate(Count, {Label, OpTag})
end,
Ops = [F(X) || X <- Operations],
list_to_tuple(lists:flatten(Ops)).
worker_init(State) ->
%% Trap exits from linked parent process; use this to ensure the driver
%% gets a chance to cleanup
process_flag(trap_exit, true),
Id = State#state.id,
DriverMod = State#state.driver_mod,
{ok, Timestamp} = {ok, {A1, A2, A3}} = DriverMod:random_seed(),
{ok, Algorithm} = DriverMod:random_algorithm(),
RngSeed = {A1 + Id, A2 + Id, A3 + Id},
rand:seed(Algorithm, RngSeed),
logger:notice("Using seed ~p with ~p for worker ~p", [Timestamp, Algorithm, Id]),
worker_idle_loop(State).
worker_idle_loop(State) ->
DriverMod = State#state.driver_mod,
receive
{init_driver, Caller} ->
%% Spin up the driver implementation
case catch DriverMod:new(State#state.id) of
{ok, DriverState} ->
Caller ! driver_ready,
ok;
Error ->
DriverState = undefined, % Make erlc happy
rcl_bench_util:exit("Failed to initialize driver: ~p", [Error])
end,
worker_idle_loop(State#state{driver_state = DriverState});
run ->
DriverMod = State#state.driver_mod,
{ok, Mode} = DriverMod:mode(),
case Mode of
{rate, max} ->
logger:notice("Starting max worker: ~p", [self()]),
max_worker_run_loop(State);
{rate, Rate} ->
%% Calculate mean inter-arrival time in in milliseconds. A
%% fixed rate worker can generate (at max) only 1k req/sec.
MeanArrival = 1000 / Rate,
logger:notice("Starting ~w ms/req fixed rate worker: ~p", [MeanArrival, self()]),
rate_worker_run_loop(State, 1 / MeanArrival)
end
end.
worker_next_op2(State, OpTag) ->
DriverMod = State#state.driver_mod,
catch DriverMod:run(OpTag,
State#state.keygen,
State#state.valgen,
State#state.driver_state).
worker_next_op(State) ->
DriverMod = State#state.driver_mod,
Next = element(rand:uniform(State#state.ops_len), State#state.ops),
{_Label, OpTag} = Next,
Start = os:timestamp(),
Result = worker_next_op2(State, OpTag),
ElapsedUs = erlang:max(0, timer:now_diff(os:timestamp(), Start)),
case Result of
{Res, DriverState} when Res == ok orelse element(1, Res) == ok ->
rcl_bench_stats:op_complete(Next, Res, ElapsedUs),
{ok, State#state{driver_state = DriverState}};
{Res, DriverState} when Res == silent orelse element(1, Res) == silent ->
{ok, State#state{driver_state = DriverState}};
{error, Reason, DriverState} ->
logger:warning("Driver encountered error"),
%% Driver encountered a recoverable error
rcl_bench_stats:op_complete(Next, {error, Reason}, ElapsedUs),
State#state.shutdown_on_error andalso rcl_bench_util:exit("(~p) shutdown on error", [1]),
{ok, State#state{driver_state = DriverState}};
{'EXIT', Reason} ->
%% Driver crashed, generate a crash error and terminate. This will take down
%% the corresponding worker which will get restarted by the appropriate supervisor.
rcl_bench_stats:op_complete(Next, {error, crash}, ElapsedUs),
%% Give the driver a chance to cleanup
catch DriverMod:terminate({'EXIT', Reason}, State#state.driver_state),
logger:warning("Driver crashed: ~p", [Reason]),
case State#state.shutdown_on_error of
true ->
rcl_bench_util:exit("(~p) shutdown on error", [2]);
false ->
crash
end;
{stop, Reason} ->
logger:notice("Driver (~p) has requested stop: ~p", [self(), Reason]),
%% Give the driver a chance to cleanup
catch DriverMod:terminate(normal, State#state.driver_state),
normal
end.
needs_shutdown(State) ->
DriverMod = State#state.driver_mod,
Parent = State#state.parent_pid,
receive
{'EXIT', Pid, _Reason} ->
case Pid of
Parent ->
%% Give the driver a chance to cleanup
catch DriverMod:terminate(normal, State#state.driver_state),
true;
_Else ->
%% catch this so that selective receive doesn't kill us
false
end
after 0 ->
false
end.
max_worker_run_loop(State) ->
case worker_next_op(State) of
{ok, State2} ->
case needs_shutdown(State2) of
true ->
ok;
false ->
max_worker_run_loop(State2)
end;
ExitReason ->
exit(ExitReason)
end.
rate_worker_run_loop(State, Lambda) ->
%% Delay between runs using exponentially distributed delays to mimic
%% queue.
timer:sleep(trunc(rcl_bench_stats:exponential(Lambda))),
case worker_next_op(State) of
{ok, State2} ->
case needs_shutdown(State2) of
true ->
ok;
false ->
rate_worker_run_loop(State2, Lambda)
end;
ExitReason ->
exit(ExitReason)
end.