Current section
Files
Jump to
Current section
Files
src/tf_master_serv.erl
-module(tf_master_serv).
-ifdef(E48).
-moduledoc false.
-endif.
-behaviour(gen_server).
%% API functions
-export([start_link/2]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
-ignore_xref([{start_link, 2}]).
-include("taskforce.hrl").
-type exec_state() :: idle | {running, Bidder :: any()}.
-record(master_state, {
patron_pid :: pid(),
patron_monitor :: reference(),
max_minion_count :: pos_integer(),
exec_state = 'idle' :: exec_state(),
tasks = [] :: [tf_task()],
consumed_task_ids = [] :: [any()],
completed = [] :: [any()],
timedout_task_ids = [] :: [any()]
}).
-type state() :: #master_state{}.
-if(?OTP_RELEASE =< 24).
-type gen_server_from() :: {pid(), reference()}.
-else.
-type gen_server_from() :: gen_server:from().
-endif.
%%%===================================================================
%%% API functions
%%%===================================================================
-spec start_link(pid(), pos_integer()) -> {ok, pid()}.
start_link(PatronPid, MaxMinionCount) ->
gen_server:start_link(?MODULE, [PatronPid, MaxMinionCount], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
-spec init([pid() | pos_integer(), ...]) -> {ok, state()}.
init([PatronPid, MaxMinionCount]) ->
State0 = #master_state{
patron_pid = PatronPid,
patron_monitor = monitor(process, PatronPid),
max_minion_count = MaxMinionCount
},
{ok, State0}.
-spec handle_call(term(), gen_server_from(), state()) ->
{reply, {ok, tf_task()} | {error, no_more_tasks}, state()}
| {noreply, state()}
| {stop, normal, state()}.
handle_call(
{do_my_bidding, #tf_bidding{} = Bidding}, Bidder, #master_state{exec_state = idle} = State
) ->
#tf_bidding{
tasks = Tasks,
timeout = BiddingTimeout
} = Bidding,
MaxMinionCount = State#master_state.max_minion_count,
MinionCount = min(length(Tasks), MaxMinionCount),
ok = spawn_minions(MinionCount),
NewExecState = {running, Bidder},
NewState = State#master_state{
exec_state = NewExecState,
tasks = Tasks
},
case Tasks of
[] ->
{stop, normal, NewState};
[_ | _] ->
erlang:send_after(BiddingTimeout, self(), bidding_timeout),
{noreply, NewState}
end;
handle_call(
consume_task,
{_MinionPid, _},
#master_state{exec_state = {running, _}, tasks = [Task | Remaining]} = State
) ->
NewState = State#master_state{
tasks = Remaining,
consumed_task_ids = [
Task#tf_task.id
| State#master_state.consumed_task_ids
]
},
{reply, {ok, Task}, NewState};
handle_call(
consume_task, {_MinionPid, _}, #master_state{exec_state = {running, _}, tasks = []} = State
) ->
{reply, {error, no_more_tasks}, State}.
-spec handle_cast(term(), state()) -> {noreply, state()} | {stop, normal, state()}.
handle_cast(
{{task_completed, TaskId}, TaskResult}, #master_state{exec_state = {running, _}} = State
) ->
NewCompleted = [{TaskId, TaskResult} | State#master_state.completed],
NewState = State#master_state{completed = NewCompleted},
stop_when_done(NewState);
handle_cast({{task_timeout, TaskId}}, #master_state{exec_state = {running, _}} = State) ->
NewTimeouts = [TaskId | State#master_state.timedout_task_ids],
NewState = State#master_state{timedout_task_ids = NewTimeouts},
stop_when_done(NewState).
-spec handle_info(term(), state()) ->
{stop, {shutdown, patron_death | bidding_timeout}, state()}.
handle_info(
{'DOWN', Reference, process, _Pid, _Reason}, #master_state{patron_monitor = Reference} = State
) ->
{stop, {shutdown, patron_death}, State};
handle_info(bidding_timeout, #master_state{exec_state = {running, _}} = State) ->
{stop, {shutdown, bidding_timeout}, State}.
-spec terminate(term(), state()) -> ok.
terminate(_Reason, #master_state{exec_state = idle} = _State) ->
ok;
terminate(_Reason, #master_state{exec_state = {running, Bidder}} = State) ->
#master_state{
tasks = NeverConsumed,
consumed_task_ids = ConsumedIds,
completed = Completed
} = State,
NeverConsumedIds = [Task#tf_task.id || Task <- NeverConsumed],
CompletedIds = [TaskId || {TaskId, _} <- Completed],
IndividualTimeouts = (ConsumedIds -- CompletedIds),
GlobalTimeouts = NeverConsumedIds,
BiddingResults = #tf_bidding_result{
completed = Completed,
individual_timeouts = IndividualTimeouts,
global_timeouts = GlobalTimeouts
},
gen_server:reply(Bidder, {ok, BiddingResults}),
ok.
-spec code_change(term(), state(), term()) -> {ok, state()}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec stop_when_done(state()) -> {noreply, state()} | {stop, normal, state()}.
stop_when_done(State) ->
case all_tasks_accounted_for(State) of
true ->
{stop, normal, State};
false ->
{noreply, State}
end.
-spec all_tasks_accounted_for(state()) -> boolean().
all_tasks_accounted_for(#master_state{
tasks = Tasks,
completed = Completed,
timedout_task_ids = Timeouts,
consumed_task_ids = ConsumedIds
}) ->
Tasks =:= [] andalso
(length(Completed) + length(Timeouts)) =:= length(ConsumedIds).
spawn_minions(MaxMinionCount) ->
Self = self(),
lists:foreach(
fun(_Id) ->
{ok, _MinionPid} = supervisor:start_child(tf_minion_sup, [Self])
end,
lists:seq(1, MaxMinionCount)
).