def __init__(self, loop=None, target=None, name=None, args=(), kwargs={}): if not callable(target): raise TypeError( f"`target` needs to be callable, not {type(target)!r}") self._state = _ProcessState() self._loop = loop or IOLoop.current(instance=False) # _keep_child_alive is the write side of a pipe, which, when it is # closed, causes the read side of the pipe to unblock for reading. Note # that it is never closed directly. The write side is closed by the # kernel when our process exits, or possibly by the garbage collector # closing the file descriptor when the last reference to # _keep_child_alive goes away. We can take advantage of this fact to # monitor from the child and exit when the parent goes away unexpectedly # (for example due to SIGKILL). This variable is otherwise unused except # for the assignment here. parent_alive_pipe, self._keep_child_alive = mp_context.Pipe( duplex=False) self._process = mp_context.Process( target=self._run, name=name, args=( target, args, kwargs, parent_alive_pipe, self._keep_child_alive, dask.config.global_config, ), ) self._name = self._process.name self._proc_finalizer = weakref.finalize(self, _asyncprocess_finalizer, self._process) self._watch_q = PyQueue() self._exit_future = Future() self._exit_callback = None self._closed = False self._start_threads()
def test_asyncprocess_child_teardown_on_parent_exit(): r"""Check that a child process started by AsyncProcess exits if its parent exits. The motivation is to ensure that if an AsyncProcess is created and the creator process dies unexpectedly (e.g, via Out-of-memory SIGKILL), the child process and resources held by it should not be leaked. The child should monitor its parent and exit promptly if the parent exits. [test process] -> [parent using AsyncProcess (dies)] -> [worker process] \ / \________ <-- child_pipe <-- ________/ """ # When child_pipe is closed, the children_alive pipe unblocks. children_alive, child_pipe = mp_context.Pipe(duplex=False) try: parent = mp_context.Process(target=_parent_process, args=(child_pipe, )) parent.start() # Close our reference to child_pipe so that the child has the only one. child_pipe.close() # Wait for the parent to exit. By the time join returns, the child # process is orphaned, and should be in the process of exiting by # itself. parent.join() # By the time we reach here,the parent has exited. The parent only exits # when the child is ready to enter the sleep, so all of the slow things # (process startup, etc) should have happened by now, even on a busy # system. A short timeout should therefore be appropriate. short_timeout = 5.0 # Poll is used to allow other tests to proceed after this one in case of # test failure. try: readable = children_alive.poll(short_timeout) except BrokenPipeError: assert WINDOWS, "should only raise on windows" # Broken pipe implies closed, which is readable. readable = True # If this assert fires, then something went wrong. Either the child # should write into the pipe, or it should exit and the pipe should be # closed (which makes it become readable). assert readable try: # This won't block due to the above 'assert readable'. result = children_alive.recv() except EOFError: pass # Test passes. except BrokenPipeError: assert WINDOWS, "should only raise on windows" # Test passes. else: # Oops, children_alive read something. It should be closed. If # something was read, it's a message from the child telling us they # are still alive! raise RuntimeError(f"unreachable: {result}") finally: # Cleanup. children_alive.close()