I've discussed this several times with @ThomasBreuer recently, just today in context of gap-packages/anupq#79 where a GAP out of memory error interrupts a computation; after exiting, the pq child process keeps running (and taking up memory/CPU).
On way to solve that would be to temporarily replace OnQuit with a version that kills any relevant subprocesses and then calls the original OnQuit; that's what e.g. SCSCP does in a somewhat similar situation:
if IsReadOnlyGlobal("OnQuit") then
MakeReadWriteGlobal("OnQuit");
BindGlobal("OriginalOnQuit", OnQuit);
fi;
OnQuit:=function()
if SCSCP_CURRENT_SESSION_STREAM <> fail then
if not IsClosedStream( SCSCP_CURRENT_SESSION_STREAM ) then
Print( "SCSCP : ", SCSCP_CURRENT_SESSION_STREAM );
CloseStream( SCSCP_CURRENT_SESSION_STREAM );
Print( " is closed\n" );
fi;
SCSCP_CURRENT_SESSION_STREAM := fail;
fi;
OriginalOnQuit();
end;
MakeReadOnlyGlobal("OnQuit");
But IMHO that doesn't scale all too well. For example it is very easy to insert code into OnQuit that itself can trigger an error, and then we must be super careful to run into an infinite loop.
Perhaps we can provide an interface for registering such an error handler, say a function InstallOnQuit that is similar to InstallAtExit:
BIND_GLOBAL( "InstallAtExit", function( func )
local f;
if not IS_FUNCTION(func) then
Error( "<func> must be a function" );
fi;
if CHECK_INSTALL_METHOD then
if not NARG_FUNC(func) in [ -1, 0 ] then
Error( "<func> must accept zero arguments" );
fi;
fi;
# Return if function has already been installed
# Use this long form to support both List and AtomicList
for f in GAPInfo.AtExitFuncs do
if f = func then
return;
fi;
od;
ADD_LIST( GAPInfo.AtExitFuncs, func );
end );
The functions registered this way are later run in PROGRAM_CLEAN_UP (note how it uses CALL_WITH_CATCH to reduce the risk of an error in the handler leading to an infinite loop; to that end, it also removes the handler function before calling it)
BIND_GLOBAL("PROGRAM_CLEAN_UP", function()
local f, funcs;
if IsBound( GAPInfo.AtExitFuncs ) and IsList( GAPInfo.AtExitFuncs ) then
if IsHPCGAP then
funcs := FromAtomicList(GAPInfo.AtExitFuncs);
else
funcs := ShallowCopy(GAPInfo.AtExitFuncs);
fi;
while not IsEmpty(funcs) do
f := Remove(funcs);
if IsFunction(f) then
CALL_WITH_CATCH(f,[]);
fi;
od;
fi;
end);
Unlike InstallOnQuit, we may have need to remove the function again later on. One could realize that by letting it return a token that can be used to remove the handler again. (Implementation wise, that "token" might simply be the function passed to InstallOnQuit, but it could also be something else)
So, we could have something like this pseudo-code:
x := ...
y := ...
token := InstallOnQuit(function()
RunSomeCleanup(x); # access to locals is possible, thanks to a closure
RemoveOnQuit(token); # ensure this
end);
RunMyComputationThatMightError(x, y);
RemoveOnQuit(token);
Note how the handler removes itself, to make sure it is removed even in case of an error.
In practice, though, very often one wants to run the cleanup handler also in case the computation ends cleanly. Things start to get annoying... So perhaps for at least the anupq example, a nice interface would be something like
RunWithErrorHandler(
function()
RunMyComputationThatMightError(x, y);
end,
function()
RunSomeCleanup(x);
end);
which internally could be implemented using InstallOnQuit etc., but would take of most of the complexity, and ensure not everyone has to remember the edge cases etc.
I've discussed this several times with @ThomasBreuer recently, just today in context of gap-packages/anupq#79 where a GAP out of memory error interrupts a computation; after exiting, the
pqchild process keeps running (and taking up memory/CPU).On way to solve that would be to temporarily replace
OnQuitwith a version that kills any relevant subprocesses and then calls the originalOnQuit; that's what e.g. SCSCP does in a somewhat similar situation:But IMHO that doesn't scale all too well. For example it is very easy to insert code into
OnQuitthat itself can trigger an error, and then we must be super careful to run into an infinite loop.Perhaps we can provide an interface for registering such an error handler, say a function
InstallOnQuitthat is similar toInstallAtExit:The functions registered this way are later run in
PROGRAM_CLEAN_UP(note how it usesCALL_WITH_CATCHto reduce the risk of an error in the handler leading to an infinite loop; to that end, it also removes the handler function before calling it)Unlike
InstallOnQuit, we may have need to remove the function again later on. One could realize that by letting it return atokenthat can be used to remove the handler again. (Implementation wise, that "token" might simply be the function passed toInstallOnQuit, but it could also be something else)So, we could have something like this pseudo-code:
Note how the handler removes itself, to make sure it is removed even in case of an error.
In practice, though, very often one wants to run the cleanup handler also in case the computation ends cleanly. Things start to get annoying... So perhaps for at least the
anupqexample, a nice interface would be something likeRunWithErrorHandler( function() RunMyComputationThatMightError(x, y); end, function() RunSomeCleanup(x); end);which internally could be implemented using
InstallOnQuitetc., but would take of most of the complexity, and ensure not everyone has to remember the edge cases etc.