Current section
Files
Jump to
Current section
Files
src/redo_block.erl
%% Manages multiple requests and timers. May or may not kill it if the task
%% takes too long, and then just start a new worker instead.
%%
%% Subscription mode isn't supported yet.
-module(redo_block).
-behaviour(gen_server).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([start_link/0, start_link/1, start_link/2,
cmd/1, cmd/2, cmd/3, shutdown/1]).
-export([reply/2]).
-record(state, {opts=[], refs, acc,
worker, worker_status=free,
timers, queue}).
-define(TIMEOUT, 5000).
%%%%%%%%%%%%%%%%%
%%% INTERFACE %%%
%%%%%%%%%%%%%%%%%
start_link() ->
start_link([]).
start_link(Name) when is_atom(Name) ->
start_link(Name, []);
start_link(Opts) when is_list(Opts) ->
start_link(?MODULE, Opts).
start_link(undefined, Opts) when is_list(Opts) ->
gen_server:start_link(?MODULE, Opts, []);
start_link(Name, Opts) when is_atom(Name), is_list(Opts) ->
gen_server:start_link({local, Name}, ?MODULE, Opts, []).
cmd(Cmd) ->
cmd(?MODULE, Cmd, ?TIMEOUT).
cmd(NameOrPid, Cmd) ->
cmd(NameOrPid, Cmd, ?TIMEOUT).
cmd(NameOrPid, Cmd, Timeout) ->
%% format commands to be sent to redis | now done in the worker
%% Packets = redo_redis_proto:package(Cmd),
gen_server:call(NameOrPid, {cmd, Cmd, Timeout}, infinity).
shutdown(NameOrPid) ->
gen_server:cast(NameOrPid, shutdown).
reply({Pid, TimerRef}, Response) ->
gen_server:cast(Pid, {done, self(), TimerRef, Response}).
%%%%%%%%%%%%%%%%%
%%% CALLBACKS %%%
%%%%%%%%%%%%%%%%%
init(Opts) ->
process_flag(trap_exit, true), % deal with worker failures
{ok, Worker} = redo:start_link(undefined, Opts),
{ok, #state{opts=Opts,
worker=Worker, worker_status=free,
timers=gb_trees:empty(), queue=queue:new()}}.
handle_call({cmd, Cmd, TimeOut}, From, S=#state{worker_status=free, timers=T, queue=Q}) ->
TimerRef = erlang:start_timer(TimeOut, self(), From),
handle_info(timeout, S#state{timers=gb_trees:insert(TimerRef, {From,Cmd}, T),
queue=queue:in(TimerRef,Q)});
handle_call({cmd, Cmd, TimeOut}, From, S=#state{worker_status=busy, timers=T, queue=Q}) ->
TimerRef = erlang:start_timer(TimeOut, self(), From),
{noreply, S#state{timers=gb_trees:insert(TimerRef, {From,Cmd}, T),
queue=queue:in(TimerRef,Q)}, 0}.
handle_cast({done, TimerRef, Result}, S=#state{timers=T, queue=Q}) ->
erlang:cancel_timer(TimerRef),
case queue:out(Q) of
{empty, _} -> % response likely came too late
{noreply, S#state{worker_status=free, refs=undefined, acc=undefined}};
{{value,TimerRef}, Queue} -> % done, drop top element
{From, _} = gb_trees:get(TimerRef, T),
gen_server:reply(From, Result),
{noreply, S#state{worker_status = free,
refs = undefined,
acc = undefined,
timers = gb_trees:delete(TimerRef, T),
queue = Queue}, 0};
{{value,_Ref}, _} -> % race condition, timed out answer?
%% Let's clean up just in case
Queue = queue:filter(fun(Ref) -> Ref =/= TimerRef end, Q),
{noreply, S#state{worker_status = free,
timers = gb_trees:delete_any(TimerRef, T),
queue = Queue}, 0}
end;
handle_cast(shutdown, State) ->
{stop, {shutdown,cast}, State}.
handle_info(timeout, S=#state{worker_status=busy}) ->
%% Task scheduled. We'll get a message when it's done
{noreply, S};
handle_info(timeout, S=#state{worker_status=free}) ->
case schedule(S) of
{ok, State} -> {noreply, State};
{error, Reason} -> {stop, {worker,Reason}, S}
end;
handle_info({timeout, TimerRef, From}, S=#state{worker=Pid, timers=T, queue=Q}) ->
case queue:out(Q) of
{empty, _} -> % potential bad timer management?
{noreply, S};
{{value,TimerRef}, Queue} -> % currently being handled
unlink(Pid),
exit(Pid, shutdown),
{From, _} = gb_trees:get(TimerRef, T),
gen_server:reply(From, {error, timeout}),
case schedule(S#state{worker = undefined,
worker_status = free,
refs = undefined,
acc = undefined,
timers = gb_trees:delete(TimerRef, T),
queue = Queue}) of
{ok, State} -> {noreply, State};
{error, Reason} -> {stop, {worker,Reason}, S}
end;
{_, _} -> % somewhere in the queue, or maybe not if raced before cancel
case gb_trees:lookup(TimerRef, T) of
none ->
{noreply, S};
{value, {From, _}} -> % same as the one we got
gen_server:reply(From, {error, timeout}),
Queue = queue:filter(fun(Ref) -> Ref =/= TimerRef end, Q),
{noreply, S#state{timers = gb_trees:delete(TimerRef, T),
queue = Queue}}
end
end;
handle_info({'EXIT', Pid, _Reason}, S=#state{worker=Pid}) ->
%% The worker died. We retry until the timeout fails and the task is pulled
%% from the queue.
case schedule(S#state{worker=undefined, worker_status=free,
refs=undefined, acc=undefined}) of
{ok, State} -> {noreply, State};
{error, Reason} -> {stop, {worker, Reason}, S}
end;
%% Handling responses from redo
handle_info({Ref, Val}, S=#state{refs=[Ref], acc=Acc, queue=Q}) ->
{value, TimerRef} = queue:peek(Q),
Result = case {Val,Acc} of % if Acc [], one single req, drop list.
{closed, []} -> {error,closed};
{closed, _} -> lists:reverse([{error,closed} | Acc]);
{_, []} -> Val;
_ -> lists:reverse([Val|Acc])
end,
gen_server:cast(self(), {done, TimerRef, Result}),
{noreply, S#state{refs=undefined, acc=undefined}};
handle_info({Ref, closed}, S=#state{refs=[Ref|Refs], acc=Acc}) ->
{noreply, S#state{refs=Refs, acc=[{error, closed} | Acc]}};
handle_info({Ref, Val}, S=#state{refs=[Ref|Refs], acc=Acc}) ->
{noreply, S#state{refs=Refs, acc=[Val | Acc]}};
handle_info({_BadRef,_}, State) ->
{noreply, State}.
terminate(_, #state{worker=undefined}) ->
ok;
terminate(_, #state{worker=Pid}) ->
exit(Pid, shutdown),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%%%%%%%%%%%%%
%%% PRIVATE %%%
%%%%%%%%%%%%%%%
schedule(S=#state{worker=undefined, opts=Opts}) ->
case redo:start_link(undefined, Opts) of
{ok, Worker} ->
schedule(S#state{worker=Worker, worker_status=free});
{stop, Err} ->
{error, Err}
end;
schedule(S=#state{worker=Pid, worker_status=free, timers=T, queue=Q}) ->
case queue:peek(Q) of
empty -> % nothing to schedule, keep idling.
{ok, S};
{value,TimerRef} ->
{_From, Cmd} = gb_trees:get(TimerRef, T),
Refs = redo:async_cmd(Pid, Cmd),
{ok, S#state{worker_status=busy, refs=Refs, acc=[]}}
end.