pythonconcurrent.futureskeyboardinterrupt

Is there any graceful way to interrupt a python concurrent future result() call?


The only mechanism I can find for handling a keyboard interrupt is to poll. Without the while loop below, the signal processing never happens and the process hangs forever.

Is there any graceful mechanism for allowing a keyboard interrupt to function when given a concurrent future object?

Putting polling loops all over my code base seems to defeat the purpose of using futures at all.

More info:

import concurrent.futures
import signal
import time
import sys


fut = concurrent.futures.Future()


def handler(signum, frame):
    print("exiting")
    fut.cancel()
    signal.signal(signal.SIGINT, orig)
    sys.exit()

orig = signal.signal(signal.SIGINT, handler)

# a time sleep is fully interruptible with a signal... but a future isnt
# time.sleep(100)

while True:
    try:
        fut.result(.03)
    except concurrent.futures.TimeoutError:
        pass

Solution

  • OK, I wrote a solution to this based on digging in cypython source and some bug reports - but it's not pretty.

    If you want to be able to interrupt a future, especially on Windows, the following seems to work:

    @contextlib.contextmanager
    def interrupt_futures(futures):  # pragma: no cover
        """Allows a list of futures to be interrupted.
    
        If an interrupt happens, they will all have their exceptions set to KeyboardInterrupt
        """
    
        # this has to be manually tested for now, because the tests interfere with the test runner
    
        def do_interr(*_):
            for ent in futures:
                try:
                    ent.set_exception(KeyboardInterrupt)
                except:
                    # if the future is already resolved or cancelled, ignore it
                    pass
            return 1
    
        if sys.platform == "win32":
            from ctypes import wintypes  # pylint: disable=import-outside-toplevel
    
            kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
    
            CTRL_C_EVENT = 0
            CTRL_BREAK_EVENT = 1
    
            HANDLER_ROUTINE = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD)
    
            @HANDLER_ROUTINE
            def handler(ctrl):
                if ctrl == CTRL_C_EVENT:
                    handled = do_interr()
                elif ctrl == CTRL_BREAK_EVENT:
                    handled = do_interr()
                else:
                    handled = False
                # If not handled, call the next handler.
                return handled
    
            if not kernel32.SetConsoleCtrlHandler(handler, True):
                raise ctypes.WinError(ctypes.get_last_error())
    
            was = signal.signal(signal.SIGINT, do_interr)
    
            yield
    
            signal.signal(signal.SIGINT, was)
    
            # restore default handler
            kernel32.SetConsoleCtrlHandler(handler, False)
        else:
            was = signal.signal(signal.SIGINT, do_interr)
            yield
            signal.signal(signal.SIGINT, was)
    

    This allows you to do this:

    with interrupt_futures([fut]):
        fut.result()
    

    For the duration of that call, interrupt signals will be intercepted and will result in the future raising a KeyboardInterrupt to the caller requesting the result - instead of simply ignoring all interrupts.