Skip to content

Commit 75495cc

Browse files
gpsheadaggieNick02claude
committed
Refactor set_forkserver_preload to use on_error parameter
Changed from a boolean raise_exceptions parameter to a more flexible on_error parameter that accepts 'ignore', 'warn', or 'fail'. - 'ignore' (default): silently ignores import failures - 'warn': emits ImportWarning from forkserver subprocess - 'fail': raises exception, causing forkserver to exit Also improved error messages by adding .add_note() to connection failures when on_error='fail' to guide users to check stderr. Updated both spawn.import_main_path() and module __import__() failure paths to support all three modes using match/case syntax. Co-authored-by: aggieNick02 <nick@pcpartpicker.com> Co-authored-by: Claude (Sonnet 4.5) <noreply@anthropic.com> Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent 5ce91ba commit 75495cc

File tree

6 files changed

+106
-44
lines changed

6 files changed

+106
-44
lines changed

Doc/library/multiprocessing.rst

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,7 +1211,7 @@ Miscellaneous
12111211
.. versionchanged:: 3.11
12121212
Accepts a :term:`path-like object`.
12131213

1214-
.. function:: set_forkserver_preload(module_names, *, raise_exceptions=False)
1214+
.. function:: set_forkserver_preload(module_names, *, on_error='ignore')
12151215

12161216
Set a list of module names for the forkserver main process to attempt to
12171217
import so that their already imported state is inherited by forked
@@ -1221,20 +1221,21 @@ Miscellaneous
12211221
For this to work, it must be called before the forkserver process has been
12221222
launched (before creating a :class:`Pool` or starting a :class:`Process`).
12231223

1224-
By default, any :exc:`ImportError` when importing modules is silently
1225-
ignored. If *raise_exceptions* is ``True``, :exc:`ImportError` exceptions
1226-
will be raised in the forkserver subprocess, causing it to exit. The
1227-
exception traceback will appear on stderr, and subsequent attempts to
1228-
create processes will fail with :exc:`EOFError` or :exc:`ConnectionError`.
1229-
Use *raise_exceptions* during development to catch import problems early.
1224+
The *on_error* parameter controls how :exc:`ImportError` exceptions during
1225+
module preloading are handled: ``'ignore'`` (default) silently ignores
1226+
failures, ``'warn'`` causes the forkserver subprocess to emit an
1227+
:exc:`ImportWarning` to stderr, and ``'fail'`` causes the forkserver
1228+
subprocess to exit with the exception traceback on stderr, making
1229+
subsequent process creation fail with :exc:`EOFError` or
1230+
:exc:`ConnectionError`.
12301231

12311232
Only meaningful when using the ``'forkserver'`` start method.
12321233
See :ref:`multiprocessing-start-methods`.
12331234

12341235
.. versionadded:: 3.4
12351236

12361237
.. versionchanged:: next
1237-
Added the *raise_exceptions* parameter.
1238+
Added the *on_error* parameter.
12381239

12391240
.. function:: set_start_method(method, force=False)
12401241

Lib/multiprocessing/context.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,15 @@ def set_executable(self, executable):
177177
from .spawn import set_executable
178178
set_executable(executable)
179179

180-
def set_forkserver_preload(self, module_names, *, raise_exceptions=False):
180+
def set_forkserver_preload(self, module_names, *, on_error='ignore'):
181181
'''Set list of module names to try to load in forkserver process.
182182
183-
If raise_exceptions is True, ImportError exceptions during preload
184-
will be raised instead of being silently ignored. Such errors will
185-
break all use of the forkserver multiprocessing context.
183+
The on_error parameter controls how import failures are handled:
184+
'ignore' (default) silently ignores failures, 'warn' emits warnings,
185+
and 'fail' raises exceptions breaking the forkserver context.
186186
'''
187187
from .forkserver import set_forkserver_preload
188-
set_forkserver_preload(module_names, raise_exceptions=raise_exceptions)
188+
set_forkserver_preload(module_names, on_error=on_error)
189189

190190
def get_context(self, method=None):
191191
if method is None:

Lib/multiprocessing/forkserver.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def __init__(self):
4242
self._inherited_fds = None
4343
self._lock = threading.Lock()
4444
self._preload_modules = ['__main__']
45-
self._raise_exceptions = False
45+
self._preload_on_error = 'ignore'
4646

4747
def _stop(self):
4848
# Method used by unit tests to stop the server
@@ -65,17 +65,22 @@ def _stop_unlocked(self):
6565
self._forkserver_address = None
6666
self._forkserver_authkey = None
6767

68-
def set_forkserver_preload(self, modules_names, *, raise_exceptions=False):
68+
def set_forkserver_preload(self, modules_names, *, on_error='ignore'):
6969
'''Set list of module names to try to load in forkserver process.
7070
71-
If raise_exceptions is True, ImportError exceptions during preload
72-
will be raised instead of being silently ignored. Such errors will
73-
break all use of the forkserver multiprocessing context.
71+
The on_error parameter controls how import failures are handled:
72+
'ignore' (default) silently ignores failures, 'warn' emits warnings,
73+
and 'fail' raises exceptions breaking the forkserver context.
7474
'''
7575
if not all(type(mod) is str for mod in modules_names):
7676
raise TypeError('module_names must be a list of strings')
77+
if on_error not in ('ignore', 'warn', 'fail'):
78+
raise ValueError(
79+
f"on_error must be 'ignore', 'warn', or 'fail', "
80+
f"not {on_error!r}"
81+
)
7782
self._preload_modules = modules_names
78-
self._raise_exceptions = raise_exceptions
83+
self._preload_on_error = on_error
7984

8085
def get_inherited_fds(self):
8186
'''Return list of fds inherited from parent process.
@@ -114,6 +119,15 @@ def connect_to_new_process(self, fds):
114119
wrapped_client, self._forkserver_authkey)
115120
connection.deliver_challenge(
116121
wrapped_client, self._forkserver_authkey)
122+
except (EOFError, ConnectionError, BrokenPipeError) as exc:
123+
# Add helpful context if forkserver likely crashed during preload
124+
if (self._preload_modules and
125+
self._preload_on_error == 'fail'):
126+
exc.add_note(
127+
"Forkserver process may have crashed during module "
128+
"preloading. Check stderr for ImportError traceback."
129+
)
130+
raise
117131
finally:
118132
wrapped_client._detach()
119133
del wrapped_client
@@ -159,8 +173,8 @@ def ensure_running(self):
159173
main_kws['sys_path'] = data['sys_path']
160174
if 'init_main_from_path' in data:
161175
main_kws['main_path'] = data['init_main_from_path']
162-
if self._raise_exceptions:
163-
main_kws['raise_exceptions'] = True
176+
if self._preload_on_error != 'ignore':
177+
main_kws['on_error'] = self._preload_on_error
164178

165179
with socket.socket(socket.AF_UNIX) as listener:
166180
address = connection.arbitrary_address('AF_UNIX')
@@ -206,7 +220,7 @@ def ensure_running(self):
206220
#
207221

208222
def main(listener_fd, alive_r, preload, main_path=None, sys_path=None,
209-
*, authkey_r=None, raise_exceptions=False):
223+
*, authkey_r=None, on_error='ignore'):
210224
"""Run forkserver."""
211225
if authkey_r is not None:
212226
try:
@@ -224,15 +238,37 @@ def main(listener_fd, alive_r, preload, main_path=None, sys_path=None,
224238
process.current_process()._inheriting = True
225239
try:
226240
spawn.import_main_path(main_path)
241+
except Exception as e:
242+
match on_error:
243+
case 'fail':
244+
raise
245+
case 'warn':
246+
import warnings
247+
warnings.warn(
248+
f"Failed to import __main__ from {main_path!r}: {e}",
249+
ImportWarning,
250+
stacklevel=2
251+
)
252+
case 'ignore':
253+
pass
227254
finally:
228255
del process.current_process()._inheriting
229256
for modname in preload:
230257
try:
231258
__import__(modname)
232-
except ImportError:
233-
if raise_exceptions:
234-
raise
235-
pass
259+
except ImportError as e:
260+
match on_error:
261+
case 'fail':
262+
raise
263+
case 'warn':
264+
import warnings
265+
warnings.warn(
266+
f"Failed to preload module {modname!r}: {e}",
267+
ImportWarning,
268+
stacklevel=2
269+
)
270+
case 'ignore':
271+
pass
236272

237273
# gh-135335: flush stdout/stderr in case any of the preloaded modules
238274
# wrote to them, otherwise children might inherit buffered data

Lib/test/test_multiprocessing_forkserver/test_preload.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import multiprocessing
44
import multiprocessing.forkserver
55
import unittest
6+
import warnings
67

78

89
class TestForkserverPreload(unittest.TestCase):
@@ -23,9 +24,9 @@ def _send_value(conn, value):
2324
"""Helper to send a value through a connection."""
2425
conn.send(value)
2526

26-
def test_preload_raise_exceptions_false_default(self):
27+
def test_preload_on_error_ignore_default(self):
2728
"""Test that invalid modules are silently ignored by default."""
28-
# With raise_exceptions=False (default), invalid module is ignored
29+
# With on_error='ignore' (default), invalid module is ignored
2930
self.ctx.set_forkserver_preload(['nonexistent_module_xyz'])
3031

3132
# Should be able to start a process without errors
@@ -40,9 +41,9 @@ def test_preload_raise_exceptions_false_default(self):
4041
self.assertEqual(result, 42)
4142
self.assertEqual(p.exitcode, 0)
4243

43-
def test_preload_raise_exceptions_false_explicit(self):
44-
"""Test that invalid modules are silently ignored with raise_exceptions=False."""
45-
self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], raise_exceptions=False)
44+
def test_preload_on_error_ignore_explicit(self):
45+
"""Test that invalid modules are silently ignored with on_error='ignore'."""
46+
self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], on_error='ignore')
4647

4748
# Should be able to start a process without errors
4849
r, w = self.ctx.Pipe(duplex=False)
@@ -56,27 +57,45 @@ def test_preload_raise_exceptions_false_explicit(self):
5657
self.assertEqual(result, 99)
5758
self.assertEqual(p.exitcode, 0)
5859

59-
def test_preload_raise_exceptions_true_breaks_context(self):
60-
"""Test that invalid modules with raise_exceptions=True breaks the forkserver."""
61-
self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], raise_exceptions=True)
60+
def test_preload_on_error_warn(self):
61+
"""Test that invalid modules emit warnings with on_error='warn'."""
62+
self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], on_error='warn')
63+
64+
# Should still be able to start a process, warnings are in subprocess
65+
r, w = self.ctx.Pipe(duplex=False)
66+
p = self.ctx.Process(target=self._send_value, args=(w, 123))
67+
p.start()
68+
w.close()
69+
result = r.recv()
70+
r.close()
71+
p.join()
72+
73+
self.assertEqual(result, 123)
74+
self.assertEqual(p.exitcode, 0)
75+
76+
def test_preload_on_error_fail_breaks_context(self):
77+
"""Test that invalid modules with on_error='fail' breaks the forkserver."""
78+
self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], on_error='fail')
6279

6380
# The forkserver should fail to start when it tries to import
6481
# The exception is raised during p.start() when trying to communicate
6582
# with the failed forkserver process
6683
r, w = self.ctx.Pipe(duplex=False)
6784
try:
6885
p = self.ctx.Process(target=self._send_value, args=(w, 42))
69-
with self.assertRaises((EOFError, ConnectionError, BrokenPipeError)):
86+
with self.assertRaises((EOFError, ConnectionError, BrokenPipeError)) as cm:
7087
p.start() # Exception raised here
88+
# Verify that the helpful note was added
89+
self.assertIn('Forkserver process may have crashed', str(cm.exception.__notes__[0]))
7190
finally:
7291
# Ensure pipes are closed even if exception is raised
7392
w.close()
7493
r.close()
7594

76-
def test_preload_valid_modules_with_raise_exceptions(self):
77-
"""Test that valid modules work fine with raise_exceptions=True."""
78-
# Valid modules should work even with raise_exceptions=True
79-
self.ctx.set_forkserver_preload(['os', 'sys'], raise_exceptions=True)
95+
def test_preload_valid_modules_with_on_error_fail(self):
96+
"""Test that valid modules work fine with on_error='fail'."""
97+
# Valid modules should work even with on_error='fail'
98+
self.ctx.set_forkserver_preload(['os', 'sys'], on_error='fail')
8099

81100
r, w = self.ctx.Pipe(duplex=False)
82101
p = self.ctx.Process(target=self._send_value, args=(w, 'success'))
@@ -89,6 +108,12 @@ def test_preload_valid_modules_with_raise_exceptions(self):
89108
self.assertEqual(result, 'success')
90109
self.assertEqual(p.exitcode, 0)
91110

111+
def test_preload_invalid_on_error_value(self):
112+
"""Test that invalid on_error values raise ValueError."""
113+
with self.assertRaises(ValueError) as cm:
114+
self.ctx.set_forkserver_preload(['os'], on_error='invalid')
115+
self.assertIn("on_error must be 'ignore', 'warn', or 'fail'", str(cm.exception))
116+
92117

93118
if __name__ == '__main__':
94119
unittest.main()

Misc/NEWS.d/next/Library/2025-11-22-20-30-00.gh-issue-101100.frksvr.rst

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Add an ``on_error`` keyword-only parameter to
2+
:func:`multiprocessing.set_forkserver_preload` to control how import failures
3+
during module preloading are handled. Accepts ``'ignore'`` (default, silent),
4+
``'warn'`` (emit :exc:`ImportWarning`), or ``'fail'`` (raise exception).
5+
Contributed by Nick Neumann.

0 commit comments

Comments
 (0)