Skip to content

Commit 48cc09f

Browse files
authored
Merge branch 'main' into c-api-pyrun-string-doc
2 parents 5096738 + 293fa34 commit 48cc09f

21 files changed

+288
-24
lines changed

Doc/library/concurrent.futures.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,30 @@ to a :class:`ProcessPoolExecutor` will result in deadlock.
415415
require the *fork* start method for :class:`ProcessPoolExecutor` you must
416416
explicitly pass ``mp_context=multiprocessing.get_context("fork")``.
417417

418+
.. method:: terminate_workers()
419+
420+
Attempt to terminate all living worker processes immediately by calling
421+
:meth:`Process.terminate <multiprocessing.Process.terminate>` on each of them.
422+
Internally, it will also call :meth:`Executor.shutdown` to ensure that all
423+
other resources associated with the executor are freed.
424+
425+
After calling this method the caller should no longer submit tasks to the
426+
executor.
427+
428+
.. versionadded:: next
429+
430+
.. method:: kill_workers()
431+
432+
Attempt to kill all living worker processes immediately by calling
433+
:meth:`Process.kill <multiprocessing.Process.kill>` on each of them.
434+
Internally, it will also call :meth:`Executor.shutdown` to ensure that all
435+
other resources associated with the executor are freed.
436+
437+
After calling this method the caller should no longer submit tasks to the
438+
executor.
439+
440+
.. versionadded:: next
441+
418442
.. _processpoolexecutor-example:
419443

420444
ProcessPoolExecutor Example

Doc/library/typing.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2398,6 +2398,10 @@ types.
23982398
.. versionchanged:: 3.11
23992399
Added support for generic namedtuples.
24002400

2401+
.. versionchanged:: next
2402+
Using :func:`super` (and the ``__class__`` :term:`closure variable`) in methods of ``NamedTuple`` subclasses
2403+
is unsupported and causes a :class:`TypeError`.
2404+
24012405
.. deprecated-removed:: 3.13 3.15
24022406
The undocumented keyword argument syntax for creating NamedTuple classes
24032407
(``NT = NamedTuple("NT", x=int)``) is deprecated, and will be disallowed

Doc/tutorial/controlflow.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,8 @@ similar to a switch statement in C, Java or JavaScript (and many
289289
other languages), but it's more similar to pattern matching in
290290
languages like Rust or Haskell. Only the first pattern that matches
291291
gets executed and it can also extract components (sequence elements
292-
or object attributes) from the value into variables.
292+
or object attributes) from the value into variables. If no case matches,
293+
none of the branches is executed.
293294

294295
The simplest form compares a subject value against one or more literals::
295296

@@ -305,7 +306,7 @@ The simplest form compares a subject value against one or more literals::
305306
return "Something's wrong with the internet"
306307

307308
Note the last block: the "variable name" ``_`` acts as a *wildcard* and
308-
never fails to match. If no case matches, none of the branches is executed.
309+
never fails to match.
309310

310311
You can combine several literals in a single pattern using ``|`` ("or")::
311312

Doc/whatsnew/3.14.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,10 @@ concurrent.futures
437437

438438
(Contributed by Gregory P. Smith in :gh:`84559`.)
439439

440+
* Add :meth:`concurrent.futures.ProcessPoolExecutor.terminate_workers` and
441+
:meth:`concurrent.futures.ProcessPoolExecutor.kill_workers` as
442+
ways to terminate or kill all living worker processes in the given pool.
443+
(Contributed by Charles Machalow in :gh:`130849`.)
440444

441445
contextvars
442446
-----------

Include/internal/pycore_opcode_metadata.h

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_uop_metadata.h

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/concurrent/futures/process.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,14 @@ class BrokenProcessPool(_base.BrokenExecutor):
626626
while a future was in the running state.
627627
"""
628628

629+
_TERMINATE = "terminate"
630+
_KILL = "kill"
631+
632+
_SHUTDOWN_CALLBACK_OPERATION = {
633+
_TERMINATE,
634+
_KILL
635+
}
636+
629637

630638
class ProcessPoolExecutor(_base.Executor):
631639
def __init__(self, max_workers=None, mp_context=None,
@@ -855,3 +863,66 @@ def shutdown(self, wait=True, *, cancel_futures=False):
855863
self._executor_manager_thread_wakeup = None
856864

857865
shutdown.__doc__ = _base.Executor.shutdown.__doc__
866+
867+
def _force_shutdown(self, operation):
868+
"""Attempts to terminate or kill the executor's workers based off the
869+
given operation. Iterates through all of the current processes and
870+
performs the relevant task if the process is still alive.
871+
872+
After terminating workers, the pool will be in a broken state
873+
and no longer usable (for instance, new tasks should not be
874+
submitted).
875+
"""
876+
if operation not in _SHUTDOWN_CALLBACK_OPERATION:
877+
raise ValueError(f"Unsupported operation: {operation!r}")
878+
879+
processes = {}
880+
if self._processes:
881+
processes = self._processes.copy()
882+
883+
# shutdown will invalidate ._processes, so we copy it right before
884+
# calling. If we waited here, we would deadlock if a process decides not
885+
# to exit.
886+
self.shutdown(wait=False, cancel_futures=True)
887+
888+
if not processes:
889+
return
890+
891+
for proc in processes.values():
892+
try:
893+
if not proc.is_alive():
894+
continue
895+
except ValueError:
896+
# The process is already exited/closed out.
897+
continue
898+
899+
try:
900+
if operation == _TERMINATE:
901+
proc.terminate()
902+
elif operation == _KILL:
903+
proc.kill()
904+
except ProcessLookupError:
905+
# The process just ended before our signal
906+
continue
907+
908+
def terminate_workers(self):
909+
"""Attempts to terminate the executor's workers.
910+
Iterates through all of the current worker processes and terminates
911+
each one that is still alive.
912+
913+
After terminating workers, the pool will be in a broken state
914+
and no longer usable (for instance, new tasks should not be
915+
submitted).
916+
"""
917+
return self._force_shutdown(operation=_TERMINATE)
918+
919+
def kill_workers(self):
920+
"""Attempts to kill the executor's workers.
921+
Iterates through all of the current worker processes and kills
922+
each one that is still alive.
923+
924+
After killing workers, the pool will be in a broken state
925+
and no longer usable (for instance, new tasks should not be
926+
submitted).
927+
"""
928+
return self._force_shutdown(operation=_KILL)

Lib/test/test_concurrent_futures/test_process_pool.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import os
2+
import queue
3+
import signal
24
import sys
35
import threading
46
import time
57
import unittest
8+
import unittest.mock
69
from concurrent import futures
710
from concurrent.futures.process import BrokenProcessPool
811

912
from test import support
1013
from test.support import hashlib_helper
14+
from test.test_importlib.metadata.fixtures import parameterize
1115

1216
from .executor import ExecutorTest, mul
1317
from .util import (
@@ -22,6 +26,21 @@ def __init__(self, mgr):
2226
def __del__(self):
2327
self.event.set()
2428

29+
TERMINATE_WORKERS = futures.ProcessPoolExecutor.terminate_workers.__name__
30+
KILL_WORKERS = futures.ProcessPoolExecutor.kill_workers.__name__
31+
FORCE_SHUTDOWN_PARAMS = [
32+
dict(function_name=TERMINATE_WORKERS),
33+
dict(function_name=KILL_WORKERS),
34+
]
35+
36+
def _put_wait_put(queue, event):
37+
""" Used as part of test_terminate_workers """
38+
queue.put('started')
39+
event.wait()
40+
41+
# We should never get here since the event will not get set
42+
queue.put('finished')
43+
2544

2645
class ProcessPoolExecutorTest(ExecutorTest):
2746

@@ -218,6 +237,107 @@ def mock_start_new_thread(func, *args, **kwargs):
218237
list(executor.map(mul, [(2, 3)] * 10))
219238
executor.shutdown()
220239

240+
def test_terminate_workers(self):
241+
mock_fn = unittest.mock.Mock()
242+
with self.executor_type(max_workers=1) as executor:
243+
executor._force_shutdown = mock_fn
244+
executor.terminate_workers()
245+
246+
mock_fn.assert_called_once_with(operation=futures.process._TERMINATE)
247+
248+
def test_kill_workers(self):
249+
mock_fn = unittest.mock.Mock()
250+
with self.executor_type(max_workers=1) as executor:
251+
executor._force_shutdown = mock_fn
252+
executor.kill_workers()
253+
254+
mock_fn.assert_called_once_with(operation=futures.process._KILL)
255+
256+
def test_force_shutdown_workers_invalid_op(self):
257+
with self.executor_type(max_workers=1) as executor:
258+
self.assertRaises(ValueError,
259+
executor._force_shutdown,
260+
operation='invalid operation'),
261+
262+
@parameterize(*FORCE_SHUTDOWN_PARAMS)
263+
def test_force_shutdown_workers(self, function_name):
264+
manager = self.get_context().Manager()
265+
q = manager.Queue()
266+
e = manager.Event()
267+
268+
with self.executor_type(max_workers=1) as executor:
269+
executor.submit(_put_wait_put, q, e)
270+
271+
# We should get started, but not finished since we'll terminate the
272+
# workers just after and never set the event.
273+
self.assertEqual(q.get(timeout=support.SHORT_TIMEOUT), 'started')
274+
275+
worker_process = list(executor._processes.values())[0]
276+
277+
Mock = unittest.mock.Mock
278+
worker_process.terminate = Mock(wraps=worker_process.terminate)
279+
worker_process.kill = Mock(wraps=worker_process.kill)
280+
281+
getattr(executor, function_name)()
282+
worker_process.join()
283+
284+
if function_name == TERMINATE_WORKERS:
285+
worker_process.terminate.assert_called()
286+
elif function_name == KILL_WORKERS:
287+
worker_process.kill.assert_called()
288+
else:
289+
self.fail(f"Unknown operation: {function_name}")
290+
291+
self.assertRaises(queue.Empty, q.get, timeout=0.01)
292+
293+
@parameterize(*FORCE_SHUTDOWN_PARAMS)
294+
def test_force_shutdown_workers_dead_workers(self, function_name):
295+
with self.executor_type(max_workers=1) as executor:
296+
future = executor.submit(os._exit, 1)
297+
self.assertRaises(BrokenProcessPool, future.result)
298+
299+
# even though the pool is broken, this shouldn't raise
300+
getattr(executor, function_name)()
301+
302+
@parameterize(*FORCE_SHUTDOWN_PARAMS)
303+
def test_force_shutdown_workers_not_started_yet(self, function_name):
304+
ctx = self.get_context()
305+
with unittest.mock.patch.object(ctx, 'Process') as mock_process:
306+
with self.executor_type(max_workers=1, mp_context=ctx) as executor:
307+
# The worker has not been started yet, terminate/kill_workers
308+
# should basically no-op
309+
getattr(executor, function_name)()
310+
311+
mock_process.return_value.kill.assert_not_called()
312+
mock_process.return_value.terminate.assert_not_called()
313+
314+
@parameterize(*FORCE_SHUTDOWN_PARAMS)
315+
def test_force_shutdown_workers_stops_pool(self, function_name):
316+
with self.executor_type(max_workers=1) as executor:
317+
task = executor.submit(time.sleep, 0)
318+
self.assertIsNone(task.result())
319+
320+
worker_process = list(executor._processes.values())[0]
321+
getattr(executor, function_name)()
322+
323+
self.assertRaises(RuntimeError, executor.submit, time.sleep, 0)
324+
325+
# A signal sent, is not a signal reacted to.
326+
# So wait a moment here for the process to die.
327+
# If we don't, every once in a while we may get an ENV CHANGE
328+
# error since the process would be alive immediately after the
329+
# test run.. and die a moment later.
330+
worker_process.join(support.SHORT_TIMEOUT)
331+
332+
# Oddly enough, even though join completes, sometimes it takes a
333+
# moment for the process to actually be marked as dead.
334+
# ... that seems a bit buggy.
335+
# We need it dead before ending the test to ensure it doesn't
336+
# get marked as an ENV CHANGE due to living child process.
337+
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
338+
if not worker_process.is_alive():
339+
break
340+
221341

222342
create_executor_tests(globals(), ProcessPoolExecutorTest,
223343
executor_mixins=(ProcessPoolForkMixin,

Lib/test/test_list.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import sys
2-
from test import list_tests
2+
import textwrap
3+
from test import list_tests, support
34
from test.support import cpython_only
5+
from test.support.import_helper import import_module
6+
from test.support.script_helper import assert_python_failure
47
import pickle
58
import unittest
69

@@ -309,5 +312,20 @@ def test_tier2_invalidates_iterator(self):
309312
a.append(4)
310313
self.assertEqual(list(it), [])
311314

315+
@support.cpython_only
316+
def test_no_memory(self):
317+
# gh-118331: Make sure we don't crash if list allocation fails
318+
import_module("_testcapi")
319+
code = textwrap.dedent("""
320+
import _testcapi, sys
321+
# Prime the freelist
322+
l = [None]
323+
del l
324+
_testcapi.set_nomemory(0)
325+
l = [None]
326+
""")
327+
_, _, err = assert_python_failure("-c", code)
328+
self.assertIn("MemoryError", err.decode("utf-8"))
329+
312330
if __name__ == "__main__":
313331
unittest.main()

Lib/test/test_typing.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8349,6 +8349,23 @@ class VeryAnnoying(metaclass=Meta): pass
83498349
class Foo(NamedTuple):
83508350
attr = very_annoying
83518351

8352+
def test_super_explicitly_disallowed(self):
8353+
expected_message = (
8354+
"uses of super() and __class__ are unsupported "
8355+
"in methods of NamedTuple subclasses"
8356+
)
8357+
8358+
with self.assertRaises(TypeError, msg=expected_message):
8359+
class ThisWontWork(NamedTuple):
8360+
def __repr__(self):
8361+
return super().__repr__()
8362+
8363+
with self.assertRaises(TypeError, msg=expected_message):
8364+
class ThisWontWorkEither(NamedTuple):
8365+
@property
8366+
def name(self):
8367+
return __class__.__name__
8368+
83528369

83538370
class TypedDictTests(BaseTestCase):
83548371
def test_basics_functional_syntax(self):

0 commit comments

Comments
 (0)