Skip to content

AssertionError: daemonic processes are not allowed to have children #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wimglenn opened this issue Feb 10, 2021 · 8 comments
Closed

Comments

@wimglenn
Copy link
Contributor

wimglenn commented Feb 10, 2021

First of all, I am aware of #31 but it was closed by the submitter without a proper resolution.

concurrent.futures.ProcessPoolExecutor doesn't have this limitation on pools within pools. It can have a nested process pool with no problem. I use the example from the docs verbatim, in a file eg.py.

>>> import pebble, concurrent.futures, eg
>>> with concurrent.futures.ProcessPoolExecutor(max_workers=1) as pool:
...     pool.submit(eg.main)
...
<Future at 0x10dd3a1f0 state=running>
112272535095293 is prime: True
112582705942171 is prime: True
112272535095293 is prime: True
115280095190773 is prime: True
115797848077099 is prime: True
1099726899285419 is prime: False

But with pebble's process pool, which I was usually able to use as a drop-in replacement/improvement, pools spawning pools is disallowed:

>>> with pebble.ProcessPool(max_workers=1) as pool:
...     pool.schedule(eg.main)
...
<ProcessFuture at 0x10dde5190 state=pending>
>>> _.result()
pebble.common.RemoteTraceback: Traceback (most recent call last):
  File ".../.venv/lib/python3.9/site-packages/pebble/common.py", line 174, in process_execute
    return function(*args, **kwargs)
  File "/private/tmp/eg.py", line 28, in main
    for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
  File "/usr/local/Cellar/[email protected]/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/process.py", line 726, in map
    results = super().map(partial(_process_chunk, fn),
  File "/usr/local/Cellar/[email protected]/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py", line 589, in map
    fs = [self.submit(fn, *args) for args in zip(*iterables)]
  File "/usr/local/Cellar/[email protected]/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py", line 589, in <listcomp>
    fs = [self.submit(fn, *args) for args in zip(*iterables)]
  File "/usr/local/Cellar/[email protected]/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/process.py", line 697, in submit
    self._adjust_process_count()
  File "/usr/local/Cellar/[email protected]/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/process.py", line 675, in _adjust_process_count
    p.start()
  File "/usr/local/Cellar/[email protected]/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/process.py", line 118, in start
    assert not _current_process._config.get('daemon'), \
AssertionError: daemonic processes are not allowed to have children


The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/[email protected]/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py", line 433, in result
    return self.__get_result()
  File "/usr/local/Cellar/[email protected]/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py", line 389, in __get_result
    raise self._exception
AssertionError: daemonic processes are not allowed to have children

Why is that? Is it only a preventative measure to prevent the possibility of "zombie grandchildren" 🧟 processes? Is there any public-facing way to lift the restriction from the pebble ProcessPool? (Pebble 4.6.0 on macOS)

Thanks for such a helpful and easy to use library!

@noxdafox
Copy link
Owner

noxdafox commented Feb 15, 2021

Hello,

pebble predates concurrent.futures and was inspired by multiprocessing.Pool. multiprocessing.Pool sets worker processes as daemon and it does so rightfully.

It is a very common practice to integrate pools within long-running services. One of the desires in such scenario is that, if something goes wrong, the service can quickly terminate so that its supervisor (supervisord, systemd, Docker or anything else) can collect the state and re-start it properly. This to guarantee the continuity of service.

Not setting processes as daemon would cause the program to deadlock if the pool is not used within a context manager and an uncaught exception shows up.

You can easily verify this issue with concurrent.futures.

import time
import multiprocessing
import concurrent.futures

def process_function():
    time.sleep(5)

pool = concurrent.futures.ProcessPoolExecutor()
# Simulate the scheduling of many long running tasks
for _ in range(1000):
    pool.submit(process_function)

raise Exception("Boom Baby!")

print("You waited the completion of 1000 tasks but, as your main program gave up, you wasted 'em all!")

You might argue that it's responsibility of the developer to correctly handle exceptions within their main loop and terminate the pool accordingly if they cannot use a context manager. Nevertheless, reality is more complex than that and you definitely don't want to come on a Monday morning just to notice your service has been hanging since you drank your first Friday night beer because of something as trivial as an uncaught exception (been there, done that).

The reason why I did not expose the daemon flag is that, when I designed Pebble, I had a simple and clean interface in mind (as the name suggests). This was in contrast to one of the popular multiprocessing alternatives at that time, billiard, which was providing an excessive amount of parameters.

Could you better elaborate the use case you have in mind? Usually piling up nested processes is a recipe for a disaster. Is there something which forces you to do so?

@wimglenn
Copy link
Contributor Author

wimglenn commented Feb 16, 2021

To elaborate on the use-case, I'm running "plugin" code provided by external party and verifying the results for correctness and performance. Imagine that the external code is required to solve a difficult equation (a simplification but the right idea), and you want to verify that it handled various inputs and edge cases correctly.

The external code is only required to supply a callable entrypoint, there is no restriction on how it should be implement a solve (in particular, it should be able to spawn worker threads or processes if it needs to). I want to have a strict timeout on the external code, which is where pebble comes in - the timeout actually works even if the code under subprocess misbehaves.

So, the issue is that using pebble's ProcessPool creates this restriction on the implementation detail of the plugin's code, preventing them from using multiprocessing.

@noxdafox
Copy link
Owner

Have you considered the issue deriving from the timeout killing the worker processes and leaving their children orphan?

This might especially prove problematic in your case where you are running third party code which might hang or burn CPU for a very long time while the main loop already timed out long ago and continued spawning new test cases. On a long run, you would find your environment with a lot of runaway processes. How would you handle those?

Example reproducing the issue.

import subprocess

import pebble

def function():
    subprocess.run(['ping', '192.168.1.1', '-c', '10'])

with pebble.ProcessPool() as pool:
    future = pool.schedule(function, timeout=3)
    future.result()

@wimglenn
Copy link
Contributor Author

I have considered it, it's what I called the "zombie grandchildren" in the original post. The possibility may need to be handled, perhaps by the top-level process auditing the grandchildren, although I would not really expect that to be the responsibility of pebble itself. And, it's sort of an outlier case on the unhappy path - for pebble's API, I would think it's not a convincing enough reason to disallow well behaved workers which are correctly managing their own children?

@noxdafox
Copy link
Owner

I would think it's not a convincing enough reason to disallow well behaved workers which are correctly managing their own children?

I think it's more of an issue of how many footguns a library should provide to its users.

I will try to see whether to expose the process daemon flag or whether to use non-daemonic processes and maintain the same behaviour through atexit.

I will also look into the other issue of yours as soon as I have some spare time,

noxdafox added a commit that referenced this issue Feb 22, 2021
noxdafox added a commit that referenced this issue Feb 23, 2021
On Linux, the child process inherits the parent file descriptors.

If the parent end of the pipes is not closed, the child will be unable
to detect whether the parent exited or not.

As a consequence, workers cannot detect when the pool process has
terminated abruptly.

Signed-off-by: Matteo Cafasso <[email protected]>
noxdafox added a commit that referenced this issue Feb 24, 2021
Signed-off-by: Matteo Cafasso <[email protected]>
noxdafox added a commit that referenced this issue Feb 24, 2021
Daemon processes do not allow spawning children due to the fact their
termination might leave the children orfan.

This limits some Use Cases in which the pool workers need to run
functions from module which internally use `multiprocessing`.

Instead of setting the worker processes as daemons, we rely on
`atexit` to terminate them on exit.

The drawback of this implementation is that if the main process exits,
granchildren might end up being orphan.

Signed-off-by: Matteo Cafasso <[email protected]>
@noxdafox
Copy link
Owner

I just pushed to master the fix which should allow your use case. I still need to test it on Windows and document the drawbacks of this implementation.

I am also planning to tackle a couple more issues in this release. In the meantime, you can clone this repo to get yourself going.

@wimglenn
Copy link
Contributor Author

I've just tried it out, works fine. Thank you Matteo

noxdafox added a commit that referenced this issue Mar 6, 2021
@noxdafox
Copy link
Owner

noxdafox commented Mar 6, 2021

Fix released in 4.6.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants