Skip to content

Add a convenient way to hook (temporarily) into OnQuit to clean up after an error #6448

Description

@fingolfin

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions