Skip to content

Commit 36ed6f6

Browse files
authored
Merge pull request #521 from 2xB/fix520
Fix errors with multiprocessing
2 parents 4c6b9c1 + f6eee56 commit 36ed6f6

File tree

4 files changed

+83
-2
lines changed

4 files changed

+83
-2
lines changed

importlib_metadata/__init__.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
NullFinder,
3636
install,
3737
)
38-
from ._functools import method_cache, pass_none
38+
from ._functools import method_cache, noop, pass_none, passthrough
3939
from ._itertools import always_iterable, bucket, unique_everseen
4040
from ._meta import PackageMetadata, SimplePath
4141
from ._typing import md_none
@@ -787,6 +787,20 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]:
787787
"""
788788

789789

790+
@passthrough
791+
def _clear_after_fork(cached):
792+
"""Ensure ``func`` clears cached state after ``fork`` when supported.
793+
794+
``FastPath`` caches zip-backed ``pathlib.Path`` objects that retain a
795+
reference to the parent's open ``ZipFile`` handle. Re-using a cached
796+
instance in a forked child can therefore resurrect invalid file pointers
797+
and trigger ``BadZipFile``/``OSError`` failures (python/importlib_metadata#520).
798+
Registering ``cache_clear`` with ``os.register_at_fork`` keeps each process
799+
on its own cache.
800+
"""
801+
getattr(os, 'register_at_fork', noop)(after_in_child=cached.cache_clear)
802+
803+
790804
class FastPath:
791805
"""
792806
Micro-optimized class for searching a root for children.
@@ -803,7 +817,8 @@ class FastPath:
803817
True
804818
"""
805819

806-
@functools.lru_cache() # type: ignore[misc]
820+
@_clear_after_fork # type: ignore[misc]
821+
@functools.lru_cache()
807822
def __new__(cls, root):
808823
return super().__new__(cls)
809824

importlib_metadata/_functools.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import functools
22
import types
3+
from typing import Callable, TypeVar
34

45

56
# from jaraco.functools 3.3
@@ -102,3 +103,33 @@ def wrapper(param, *args, **kwargs):
102103
return func(param, *args, **kwargs)
103104

104105
return wrapper
106+
107+
108+
# From jaraco.functools 4.4
109+
def noop(*args, **kwargs):
110+
"""
111+
A no-operation function that does nothing.
112+
113+
>>> noop(1, 2, three=3)
114+
"""
115+
116+
117+
_T = TypeVar('_T')
118+
119+
120+
# From jaraco.functools 4.4
121+
def passthrough(func: Callable[..., object]) -> Callable[[_T], _T]:
122+
"""
123+
Wrap the function to always return the first parameter.
124+
125+
>>> passthrough(print)('3')
126+
3
127+
'3'
128+
"""
129+
130+
@functools.wraps(func)
131+
def wrapper(first: _T, *args, **kwargs) -> _T:
132+
func(first, *args, **kwargs)
133+
return first
134+
135+
return wrapper # type: ignore[return-value]

newsfragments/520.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed errors in FastPath under fork-multiprocessing.

tests/test_zip.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import multiprocessing
2+
import os
13
import sys
24
import unittest
35

46
from importlib_metadata import (
7+
FastPath,
58
PackageNotFoundError,
69
distribution,
710
distributions,
@@ -47,6 +50,37 @@ def test_one_distribution(self):
4750
dists = list(distributions(path=sys.path[:1]))
4851
assert len(dists) == 1
4952

53+
@unittest.skipUnless(
54+
hasattr(os, 'register_at_fork')
55+
and 'fork' in multiprocessing.get_all_start_methods(),
56+
'requires fork-based multiprocessing support',
57+
)
58+
def test_fastpath_cache_cleared_in_forked_child(self):
59+
zip_path = sys.path[0]
60+
61+
FastPath(zip_path)
62+
assert FastPath.__new__.cache_info().currsize >= 1
63+
64+
ctx = multiprocessing.get_context('fork')
65+
parent_conn, child_conn = ctx.Pipe()
66+
67+
def child(conn, root):
68+
try:
69+
before = FastPath.__new__.cache_info().currsize
70+
FastPath(root)
71+
after = FastPath.__new__.cache_info().currsize
72+
conn.send((before, after))
73+
finally:
74+
conn.close()
75+
76+
proc = ctx.Process(target=child, args=(child_conn, zip_path))
77+
proc.start()
78+
child_conn.close()
79+
cache_sizes = parent_conn.recv()
80+
proc.join()
81+
82+
self.assertEqual(cache_sizes, (0, 1))
83+
5084

5185
class TestEgg(TestZip):
5286
def setUp(self):

0 commit comments

Comments
 (0)