Current section
Files
Jump to
Current section
Files
src/futureFfi.erl
% Copyright 2025 BradBot_1
% Licensed under the Apache License, Version 2.0 (the "License");
% you may not use this file except in compliance with the License.
% You may obtain a copy of the License at
% http://www.apache.org/licenses/LICENSE-2.0
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS,
% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
% See the License for the specific language governing permissions and
% limitations under the License.
-module(futureFfi).
-behaviour(gen_server).
-export([is_complete/1]).
-export([status/1]).
-export([release/1]).
-export([create_failure_handler/2]).
-export([wait/1]).
-export([auto_release/1]).
-export([once_complete/2]).
-export([start/1]).
-export([start/2]).
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([terminate/2]).
-export([code_change/3]).
%% If the future has completed, IE a result is available or the task failed
is_complete(Instance) when is_pid(Instance) ->
case catch gen_server:call(Instance, is_complete) of
{'EXIT', _} -> true;
Result -> Result
end.
%% Returns the current status of the future
%%
%% Possible statuses are:
%% - {completed, {ok | error, Value}}
%% - running
%% - {failed, binstring()}
%% - released
status(Instance) when is_pid(Instance) ->
case catch gen_server:call(Instance, status) of
{'EXIT', _} -> released;
Status -> Status
end.
%% Kills the process (only the gen_server process, not the child process)
release(Instance) when is_pid(Instance) ->
try
gen_server:cast(Instance, release),
nil
catch
_:_ -> nil
end.
%% Creates a process that will execute the given function when the future fails
create_failure_handler(Instance, Handler) when is_pid(Instance), is_function(Handler, 1) ->
try
gen_server:cast(Instance, {create_failure_handler, Handler}),
Instance
catch
_:_ -> Instance
end.
%% Blocks execution until the future is complete
wait(Instance) when is_pid(Instance) ->
case catch gen_server:call(Instance, get_child) of
{'EXIT', _} ->
ok;
nil ->
ok;
ChildPid when is_pid(ChildPid) ->
MonitorRef = monitor(process, ChildPid),
receive
{'DOWN', MonitorRef, process, ChildPid, _Reason} ->
ok
end;
_ ->
error
end.
auto_release(Instance) when is_pid(Instance) ->
spawn(fun() ->
wait(Instance),
release(Instance)
end),
Instance.
once_complete(Instance, Executable) when is_pid(Instance), is_function(Executable, 1) ->
spawn(fun() ->
wait(Instance),
Executable(status(Instance))
end),
Instance.
start({ok, Value}) ->
{ok, Pid} = gen_server:start(?MODULE, {complete, {ok, Value}}, []),
Pid;
start({error, Reason}) ->
{ok, Pid} = gen_server:start(?MODULE, {complete, {error, Reason}}, []),
Pid;
start({failed, Reason}) ->
{ok, Pid} = gen_server:start(?MODULE, {complete, {failed, Reason}}, []),
Pid.
start(Executable, StartingValue) when is_function(Executable, 1) ->
{ok, Pid} = gen_server:start(?MODULE, {run, Executable, StartingValue}, []),
Pid;
start(Instance, Executable) when is_pid(Instance), is_function(Executable, 1) ->
case catch gen_server:call(Instance, {start, Executable}) of
{'EXIT', _} ->
{ok, Pid} = gen_server:start(?MODULE, {complete, {failed, <<"Prior future was released before initialisation"/utf8>>}}, []),
Pid;
Result -> Result
end.
init({complete, Result}) when is_tuple(Result) ->
case Result of
{ok, _} -> {ok, Result};
{error, _} -> {ok, Result};
{failed, _} -> {ok, Result};
_ -> {error, <<"Invalid result (not ok or error)"/utf8>>}
end;
init({run, Executable, StartingValue}) when is_function(Executable, 1) ->
process_flag(trap_exit, true),
Parent = self(),
{ChildPid, MonitorRef} = spawn_monitor(fun() ->
Result = try
Executable(StartingValue)
catch
_:ErrorReason ->
{caught, ErrorReason}
end,
exit({shutdown, case catch gen_server:call(Parent, {future_completed, Result}) of
{'EXIT', _} -> % Parent process was released, will pass on value to chained futures
Result;
ok ->
Result;
{error, Reason} ->
{failed, Reason};
{failed, Reason} ->
{failed, Reason};
_ ->
{failed, <<"Failed to send result to parent process"/utf8>>}
end})
end),
{ok, {waiting, ChildPid, MonitorRef}}.
handle_call({future_completed, Result}, _From, {waiting, ChildPid, MonitorRef}) when is_pid(ChildPid), is_reference(MonitorRef) ->
demonitor(MonitorRef, [flush]),
case Result of
{ok, _} ->
{reply, ok, Result};
{error, _} ->
{reply, ok, Result};
{failed, _} ->
{reply, ok, Result};
{caught, Reason} when is_binary(Reason) ->
Message = {failed, Reason},
{reply, Message, Message};
{caught, #{gleam_error := panic, message := Reason}} when is_binary(Reason) ->
Message = {failed, Reason},
{reply, Message, Message};
{caught, Reason} ->
Converted = list_to_binary(io_lib:format("~p", [Reason])),
Message = {failed, <<"Subprocess caught an exception: "/utf8, Converted/binary>>},
{reply, Message, Message};
_ ->
Message = {failed, <<"Subprocess returned an invalid result (not ok or error tuple)"/utf8>>},
{reply, Message, Message}
end;
handle_call({future_completed, _Result}, _From, State) ->
{reply, {failed, <<"Subprocess contact was unexpected, result will be lost"/utf8>>}, State};
handle_call(is_complete, _From, State) ->
{reply, case State of
{ok, _} -> true;
{error, _} -> true;
{failed, _} -> true;
_ -> false
end, State};
handle_call(status, _From, State) ->
{reply, case State of
{ok, _} -> {completed, State};
{error, _} -> {completed, State};
{failed, _} -> State;
{waiting, _, _} -> running;
_ -> {failed, <<"Unkown state"/utf8>>}
end, State};
handle_call({start, Executable}, _From, State) when is_function(Executable, 1) ->
{reply, case State of
{failed, Reason} ->
{ok, Pid} = gen_server:start(?MODULE, {complete, {failed, <<"Prior future failed: "/utf8, Reason/binary>>}}, []),
Pid;
{ok, _} ->
{ok, Pid} = gen_server:start(?MODULE, {run, Executable, State}, []),
Pid;
{error, _} ->
{ok, Pid} = gen_server:start(?MODULE, {run, Executable, State}, []),
Pid;
{waiting, ChildPid, _} when is_pid(ChildPid) ->
{ok, Pid} = gen_server:start(?MODULE, {run, fun({ProvidedChildPid, ProvidedExecutable}) ->
MonitorRef = monitor(process, ProvidedChildPid),
receive
{'DOWN', MonitorRef, process, ProvidedChildPid, Reason} ->
case Reason of
{shutdown, Result} ->
case Result of
{ok, _} ->
ProvidedExecutable(Result);
{error, _} ->
ProvidedExecutable(Result);
{failed, FailureReason} when is_binary(FailureReason) ->
{failed, <<"Prior future failed: "/utf8, FailureReason/binary>>};
_ ->
{failed, <<"Prior future failed: Invalid result from subprocess"/utf8>>}
end;
_ ->
{noreply, PriorState} = handle_info({'DOWN', MonitorRef, process, ProvidedChildPid, Reason}, {waiting, ProvidedChildPid, MonitorRef}),
case PriorState of
{failed, Reason} ->
{failed, <<"Prior future failed: "/utf8, Reason/binary>>};
_ ->
{failed, <<"Prior future failed: Invalid handle_info response"/utf8>>}
end
end
end
end, {ChildPid, Executable}}, []),
Pid;
_ ->
{ok, Pid} = gen_server:start(?MODULE, {complete, {failed, <<"Prior future failed: Invalid prior state"/utf8>>}}, []),
Pid
end, State};
handle_call(get_child, _From, State) ->
{reply, case State of
{waiting, ChildPid, _} when is_pid(ChildPid) ->
ChildPid;
_ ->
nil
end, State};
handle_call(_Request, _From, State) ->
{stop, {error, unsupported_operation}, State}.
handle_cast(release, State) ->
{stop, {shutdown, State}, State};
handle_cast({create_failure_handler, Handler}, State) when is_function(Handler, 1) ->
case State of
{failed, Reason} ->
spawn(fun() ->
Handler(Reason)
end);
{waiting, ChildPid, _} when is_pid(ChildPid) ->
spawn(fun() ->
MonitorRef = monitor(process, ChildPid),
receive
{'DOWN', MonitorRef, process, ChildPid, Reason} ->
case Reason of
{shutdown, Result} ->
case Result of
{failed, FailedReason} ->
Handler(FailedReason);
_ -> ok
end;
_ ->
{noreply, PriorState} = handle_info({'DOWN', MonitorRef, process, ChildPid, Reason}, {waiting, ChildPid, MonitorRef}),
case PriorState of
{failed, Reason} ->
Handler(Reason);
_ ->
Handler(<<"Invalid handle_info response"/utf8>>)
end
end
end
end);
_ ->
ok
end,
{noreply, State};
handle_cast(_Request, State) ->
{noreply, State}.
handle_info(Info, {waiting, ChildPid, MonitorRef}) when is_pid(ChildPid), is_reference(MonitorRef) ->
case Info of
{'DOWN', MonitorRef, process, ChildPid, Reason} ->
case Reason of
{shutdown, _} -> % Ignore
{noreply, {waiting, ChildPid, MonitorRef}};
normal ->
{noreply, {failed, <<"Subprocess exited abnormally"/utf8>>}};
noproc ->
{noreply, {failed, <<"Subprocess was never spawned (unreachable)"/utf8>>}};
FailureReason when is_binary(FailureReason) ->
{noreply, {failed, FailureReason}};
_ ->
{noreply, {failed, <<"Subprocess returned an invalid exit reason (not a binary)"/utf8>>}}
end;
_ -> % Ignore
{noreply, {waiting, ChildPid, MonitorRef}}
end;
handle_info(_Info, Result) when is_tuple(Result) ->
{noreply, Result}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.