%%%------------------------------------------------------------------- %%% @doc %%% Task utility. %%% %%% Conveniences for spawning and awaiting tasks. %%% %%% This module is based on %%% Elixir Task %%% @end %%%------------------------------------------------------------------- -module(shards_task). %% Task API -export([start/1, start/2, start/3]). -export([start_link/1, start_link/2, start_link/3]). -export([async/1, async/2, async/3]). -export([await/1, await/2]). %% Task Supervised API -export([sup_start/2, sup_start_link/2, sup_start_link/4]). -export([sup_spawn_link/3, sup_spawn_link/4]). -export([reply/4, noreply/2]). -define(TIMEOUT, 5000). %%%=================================================================== %%% Types %%%=================================================================== %% @type task() = #{ %% pid => pid() | nil, %% ref => ref() | nil, %% owner => pid() | nil %% }. %% %% Task definition. %% %% It contains these fields: %% -type task() :: #{ pid => pid() | nil, ref => reference() | nil, owner => pid() | nil }. %% @type link() = link | monitor | nolink. %% %% Process link options. -type link() :: link | monitor | nolink. %% @type info() = {node(), pid() | atom()}. %% %% Process info. -type proc_info() :: {node(), pid() | atom()}. %% @type task_fun() = fun(() -> term()) | fun((term()) -> term()). %% %% Task Function. -type task_fun() :: fun(() -> term()) | fun((term()) -> term()). %% @type callback() = {module(), atom(), [term()]}. %% %% MFA Callback. -type callback() :: {module(), atom(), [term()]}. %% Exported types -export_type([task/0, link/0, proc_info/0, task_fun/0, callback/0]). %%%=================================================================== %%% Task API %%%=================================================================== %% @doc %% Starts a task. %% %% This is only used when the task is used for side-effects %% (i.e. no interest in the returned result) and it should not %% be linked to the current process. %% @end -spec start(task_fun()) -> {ok, pid()}. start(Fun) -> start(erlang, apply, [Fun, []]). %% @doc %% Starts a task. %% %% This is only used when the task is used for side-effects %% (i.e. no interest in the returned result) and it should not %% be linked to the current process. %% @end -spec start(task_fun(), [term()]) -> {ok, pid()}. start(Fun, Args) -> start(erlang, apply, [Fun, Args]). %% @doc %% Starts a task. %% %% This is only used when the task is used for side-effects %% (i.e. no interest in the returned result) and it should not %% be linked to the current process. %% @end -spec start(module(), atom(), [term()]) -> {ok, pid()}. start(Mod, Fun, Args) -> sup_start(get_info(self()), {Mod, Fun, Args}). %% @doc %% Starts a task as part of a supervision tree. %% @end -spec start_link(task_fun()) -> {ok, pid()}. start_link(Fun) -> start_link(erlang, apply, [Fun, []]). %% @doc %% Starts a task as part of a supervision tree. %% @end -spec start_link(task_fun(), [term()]) -> {ok, pid()}. start_link(Fun, Args) -> start_link(erlang, apply, [Fun, Args]). %% @doc %% Starts a task as part of a supervision tree. %% @end -spec start_link(module(), atom(), [term()]) -> {ok, pid()}. start_link(Mod, Fun, Args) -> sup_start_link(get_info(self()), {Mod, Fun, Args}). %% @equiv async(erlang, apply, [Fun, []]) async(Fun) -> async(erlang, apply, [Fun, []]). %% @equiv async(erlang, apply, [Fun, Args]) async(Fun, Args) -> async(erlang, apply, [Fun, Args]). %% @doc %% Starts a task that must be awaited on. %% %% A `task()' type is returned containing the relevant information. %% Developers must eventually call `await/2' on the returned task. %% @end -spec async(module(), atom(), [term()]) -> task(). async(Mod, Fun, Args) -> MFA = {Mod, Fun, Args}, Owner = self(), Pid = sup_spawn_link(Owner, get_info(Owner), MFA), Ref = monitor(process, Pid), Pid ! {Owner, Ref}, #{pid => Pid, ref => Ref, owner => Owner}. %% @equiv await(Task, 5000) await(Task) -> await(Task, 5000). %% @doc %% Awaits a task reply. %% %% A timeout, in milliseconds, can be given with default value %% of `5000'. In case the task process dies, this function will %% exit with the same reason as the task. %% %% If the timeout is exceeded, `await' will exit, however, %% the task will continue to run. When the calling process exits, its %% exit signal will terminate the task if it is not trapping exits. %% %% This function assumes the task's monitor is still active or the monitor's %% `DOWN' message is in the message queue. If it has been demonitored, or the %% message already received, this function may wait for the duration of the %% timeout awaiting the message. %% %% This function will always exit and demonitor if the task crashes or if %% it times out, so the task can not be used again. %% @end -spec await(task(), timeout()) -> term() | no_return(). await(#{owner := Owner} = Task, _) when Owner /= self() -> throw({invalid_owner_error, Task}); await(#{ref := Ref} = Task, Timeout) -> receive {Ref, Reply} -> demonitor(Ref, [flush]), Reply; {'DOWN', Ref, _, Proc, Reason} -> exit({reason(Reason, Proc), {?MODULE, await, [Task, Timeout]}}) after Timeout -> demonitor(Ref, [flush]), exit({timeout, {?MODULE, await, [Task, Timeout]}}) end. %%%=================================================================== %%% Task Supervised API %%%=================================================================== %% @hidden -spec sup_start(proc_info(), callback()) -> {ok, pid()}. sup_start(Info, Fun) -> {ok, proc_lib:spawn(?MODULE, noreply, [Info, Fun])}. %% @hidden -spec sup_start_link(proc_info(), callback()) -> {ok, pid()}. sup_start_link(Info, Fun) -> {ok, proc_lib:spawn_link(?MODULE, noreply, [Info, Fun])}. %% @hidden -spec sup_start_link(pid(), link(), proc_info(), callback()) -> {ok, pid()}. sup_start_link(Caller, Link, Info, Fun) -> {ok, sup_spawn_link(Caller, Link, Info, Fun)}. %% @hidden %% @equiv sup_spawn_link(Caller, nolink, Info, Fun) sup_spawn_link(Caller, Info, Fun) -> sup_spawn_link(Caller, nolink, Info, Fun). %% @hidden -spec sup_spawn_link(pid(), link(), proc_info(), callback()) -> pid(). sup_spawn_link(Caller, Link, Info, Fun) -> proc_lib:spawn_link(?MODULE, reply, [Caller, Link, Info, Fun]). %% @hidden -spec reply(pid(), link(), proc_info(), callback()) -> term() | no_return(). reply(Caller, Link, Info, MFA) -> initial_call(MFA), case Link of link -> link(Caller), reply(Caller, nil, ?TIMEOUT, Info, MFA); monitor -> MRef = monitor(process, Caller), reply(Caller, MRef, ?TIMEOUT, Info, MFA); nolink -> reply(Caller, nil, infinity, Info, MFA) end. %% @hidden -spec noreply(proc_info(), callback()) -> term() | no_return(). noreply(Info, MFA) -> initial_call(MFA), do_apply(Info, MFA). %%%=================================================================== %%% Internal functions %%%=================================================================== %% @private reply(Caller, MRef, Timeout, Info, MFA) -> receive {Caller, Ref} -> case MRef /= nil of true -> demonitor(MRef, [flush]); _ -> ok end, Caller ! {Ref, do_apply(Info, MFA)}; {'DOWN', MRef, _, _, Reason} -> exit(Reason) after Timeout -> exit(timeout) end. %% @private do_apply(Info, {Module, Fun, Args} = MFA) -> try apply(Module, Fun, Args) catch error:Value -> Reason = {Value, erlang:get_stacktrace()}, exit(Info, MFA, Reason, Reason); throw:Value -> Reason = {{nocatch, Value}, erlang:get_stacktrace()}, exit(Info, MFA, Reason, Reason); exit:Value -> exit(Info, MFA, {Value, erlang:get_stacktrace()}, Value) end. %% @private get_info(Self) -> Info = case process_info(Self, registered_name) of {registered_name, Name} -> Name; [] -> self() end, {node(), Info}. %% @private initial_call(MFA) -> put('$initial_call', get_initial_call(MFA)). %% @private get_initial_call({erlang, apply, [Fun, Args]}) when is_function(Fun, length(Args)) -> {module, Module} = erlang:fun_info(Fun, module), {name, Name} = erlang:fun_info(Fun, name), {Module, Name, length(Args)}; get_initial_call({Mod, Fun, Args}) -> {Mod, Fun, length(Args)}. %% @private -spec exit(proc_info(), callback(), term(), term()) -> no_return(). exit(_Info, _MFA, _LogReason, Reason) when Reason == normal orelse Reason == shutdown orelse (tuple_size(Reason) == 2 andalso element(1, Reason) == shutdown) -> exit(Reason); exit(Info, MFA, LogReason, Reason) -> {Fun, Args} = get_running(MFA), error_logger:format( "\e[31m" ++ "** Task ~p terminating~n" ++ "** Started from ~p~n" ++ "** When function == ~p~n" ++ "** arguments == ~p~n" ++ "** Reason for termination == ~n" ++ "** ~p~n" ++ "\e[0m", [self(), get_from(Info), Fun, Args, get_reason(LogReason)]), exit(Reason). %% @private get_from({Node, PidOrName}) when Node == node() -> PidOrName; get_from(Other) -> Other. %% @private get_running({erlang, apply, [Fun, Args]}) when is_function(Fun, length(Args)) -> {Fun, Args}; get_running({Mod, Fun, Args}) -> {erlang:make_fun(Mod, Fun, length(Args)), Args}. %% @private get_reason({undef, [{Mod, Fun, Args, _Info} | _] = Stacktrace} = Reason) when is_atom(Mod) and is_atom(Fun) -> FunExported = fun (M, F, A) when is_list(A) -> erlang:function_exported(M, F, length(A)); (M, F, A) when is_integer(A) -> erlang:function_exported(M, F, A) end, case code:is_loaded(Mod) of false -> {module_could_not_be_loaded, Stacktrace}; _ when is_list(Args); is_integer(Args) -> case FunExported(Mod, Fun, Args) of false -> {function_not_exported, Stacktrace}; _ -> Reason end; _ -> Reason end; get_reason(Reason) -> Reason. %% @private reason(noconnection, Proc) -> {nodedown, monitor_node(Proc)}; reason(Reason, _) -> Reason. %% @private monitor_node(Pid) when is_pid(Pid) -> node(Pid); monitor_node({_, Node}) -> Node.