Skip to content
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

Repeated creation of ProcessPoolExecutor leads to "Too many open files" #124706

Open
JelleZijlstra opened this issue Sep 27, 2024 · 5 comments
Open
Labels
topic-multiprocessing type-bug An unexpected behavior, bug, or error

Comments

@JelleZijlstra
Copy link
Member

JelleZijlstra commented Sep 27, 2024

Bug report

Bug description:

Running this code:

import concurrent.futures.process
for _ in range(100):
    x = concurrent.futures.process.ProcessPoolExecutor()
    x.submit(lambda: 42)

Results (most of the time) in an error indicating the system has run out of file descriptors, for example:

Traceback (most recent call last):
  File "<python-input-3>", line 4, in <module>
    x.submit(lambda: 42)
    ~~~~~~~~^^^^^^^^^^^^
  File "/Users/jelle/.pyenv/versions/3.13.0rc2/lib/python3.13/concurrent/futures/process.py", line 811, in submit
    self._adjust_process_count()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/Users/jelle/.pyenv/versions/3.13.0rc2/lib/python3.13/concurrent/futures/process.py", line 770, in _adjust_process_count
    self._spawn_process()
    ~~~~~~~~~~~~~~~~~~~^^
  File "/Users/jelle/.pyenv/versions/3.13.0rc2/lib/python3.13/concurrent/futures/process.py", line 788, in _spawn_process
    p.start()
    ~~~~~~~^^
  File "/Users/jelle/.pyenv/versions/3.13.0rc2/lib/python3.13/multiprocessing/process.py", line 121, in start
    self._popen = self._Popen(self)
                  ~~~~~~~~~~~^^^^^^
  File "/Users/jelle/.pyenv/versions/3.13.0rc2/lib/python3.13/multiprocessing/context.py", line 289, in _Popen
    return Popen(process_obj)
  File "/Users/jelle/.pyenv/versions/3.13.0rc2/lib/python3.13/multiprocessing/popen_spawn_posix.py", line 32, in __init__
    super().__init__(process_obj)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
  File "/Users/jelle/.pyenv/versions/3.13.0rc2/lib/python3.13/multiprocessing/popen_fork.py", line 20, in __init__
    self._launch(process_obj)
    ~~~~~~~~~~~~^^^^^^^^^^^^^
  File "/Users/jelle/.pyenv/versions/3.13.0rc2/lib/python3.13/multiprocessing/popen_spawn_posix.py", line 53, in _launch
    parent_r, child_w = os.pipe()
                        ~~~~~~~^^
OSError: [Errno 24] Too many open files

The exact error varies. For example, one time the error happened on an os.getcwd() call. One time one of the futures printed as <Future at 0x1035b9950 state=finished raised PicklingError>.

I originally encountered this in the CI for Black, where it only happened on MacOS in 3.13, but the code sample above raises errors on 3.11 and 3.12 too when I run it locally. Still, it's possible that the GC changes in 3.13 make the problem more frequent.

Adding gc.collect() to the loop body fixes the problem. This suggests there is some cycle involving the executor that isn't getting collected until the GC kicks in.

CPython versions tested on:

3.11, 3.12, 3.13

Operating systems tested on:

macOS

@JelleZijlstra JelleZijlstra added the type-bug An unexpected behavior, bug, or error label Sep 27, 2024
@z764969689
Copy link
Contributor

z764969689 commented Sep 30, 2024

I can reproduce the issue by setting the maximum number of open files using ulimit -n to 256 on python 3.12. It just might be the fd limit, simply enlarging it should work.
While it still makes sense that the gc in 3.13 varies and is not able to close the files fast enough to make the issue happen more frequently. Probably need someone who masters gc in 3.13 to help with deeper analysis.

@steve-anunknown
Copy link

I can reproduce the issue by setting the maximum number of open files using ulimit -n to 256 on python 3.12. It just might be the fd limit, simply enlarging it should work. While it still makes sense that the gc in 3.13 varies and is not able to close the files fast enough to make the issue happen more frequently. Probably need someone who masters gc in 3.13 to help with deeper analysis.

If I understand correctly, we rely on the garbage collector to close the files fast enough? Is this correct? Shouldn't the closing of the files be handled by the library manually?

@JelleZijlstra
Copy link
Member Author

Yes, what I'd expect to happen is that when each ProcessPoolExecutor created in my loop loses its last reference, all resources associated with it get destroyed, and therefore the number of file descriptors (or whatever other resources) in use by the program stays the same across iterations of the loop.

@z764969689
Copy link
Contributor

z764969689 commented Jan 4, 2025

Yes, what I'd expect to happen is that when each ProcessPoolExecutor created in my loop loses its last reference, all resources associated with it get destroyed, and therefore the number of file descriptors (or whatever other resources) in use by the program stays the same across iterations of the loop.

If you want to release the resources applied by each ProcessPoolExecutor, you have to either call the shutdown() function explicitly or use the context manager syntax, here are the sample codes:

import concurrent.futures.process
if __name__ == '__main__':
    for _ in range(100):
        with concurrent.futures.process.ProcessPoolExecutor() as x:
            x.submit(lambda: 42)

        x = concurrent.futures.process.ProcessPoolExecutor()
        x.submit(lambda: 42)
        x.shutdown()

In these ways, the error would not occur, and I believe the shutdown() function also handles the fd closing. It seems nothing related to gc, just closing the fds during the loop would work. Sorry for my shallow understanding at the beginning :)

@z764969689
Copy link
Contributor

z764969689 commented Jan 4, 2025

In terms of why gc.collect() works, I guess gc is clearing those ProcessPoolExecutors and their related attributes that have not been referenced by the variable 'x' since the 2nd iteration in the loop. And explicitly closing fds is better and more elegant from my side. For more details, please check the doc for how to correctly use the ProcessPoolExecutor: https://docs.python.org/3/library/concurrent.futures.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic-multiprocessing type-bug An unexpected behavior, bug, or error
Projects
Status: No status
Development

No branches or pull requests

4 participants