Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-billiard for openSUSE:Factory
checked in at 2026-10-26 10:00:25
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-billiard (Old)
and /work/SRC/openSUSE:Factory/.python-billiard.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-billiard"
Mon Oct 26 10:00:25 2026 rev:31 rq:1392395 version:4.3.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-billiard/python-billiard.changes
2026-09-28 10:44:35.693282609 +0200
+++
/work/SRC/openSUSE:Factory/.python-billiard.new.2252680/python-billiard.changes
2026-10-26 10:01:17.337950894 +0100
@@ -1,0 +2,12 @@
+Sun Oct 25 11:59:46 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 4.3.1:
+ * Reject bool for Pool numeric args (processes, timeouts,
+ limits) - #460
+ * Reject fewer than one pool process - #462
+ * Reject a non-positive maxtasksperchild in Pool - #465
+ * Refuse to close a Process that is still running -(#467)
+ * Raise ValueError when using a closed Queue (#466)
+ * Do not report a shutdown SystemExit as the job's result
+
+-------------------------------------------------------------------
Old:
----
billiard-4.3.0.tar.gz
New:
----
billiard-4.3.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-billiard.spec ++++++
--- /var/tmp/diff_new_pack.fziNdJ/_old 2026-10-26 10:01:18.110983326 +0100
+++ /var/tmp/diff_new_pack.fziNdJ/_new 2026-10-26 10:01:18.115983536 +0100
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-billiard
-Version: 4.3.0
+Version: 4.3.1
Release: 0
Summary: Python multiprocessing fork
License: BSD-3-Clause
++++++ billiard-4.3.0.tar.gz -> billiard-4.3.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/CHANGES.txt
new/billiard-4.3.1/CHANGES.txt
--- old/billiard-4.3.0/CHANGES.txt 2026-09-17 10:30:48.366935500 +0200
+++ new/billiard-4.3.1/CHANGES.txt 2026-10-05 08:31:02.569429200 +0200
@@ -1,3 +1,13 @@
+4.3.1 - 2026-10-05
+------------------
+- Reject bool for Pool numeric args (processes, timeouts, limits) - #460
+- Reject fewer than one pool process - #462
+- Reject a non-positive maxtasksperchild in Pool - #465
+- Refuse to close a Process that is still running -(#467)
+- Raise ValueError when using a closed Queue (#466)
+- Do not report a shutdown SystemExit as the job's result (#464)
+
+
4.3.0 - 2026-09-26
------------------
- Propagate MemoryError from pool callbacks
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/PKG-INFO new/billiard-4.3.1/PKG-INFO
--- old/billiard-4.3.0/PKG-INFO 2026-09-17 10:35:43.591227800 +0200
+++ new/billiard-4.3.1/PKG-INFO 2026-10-05 08:37:27.369151400 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: billiard
-Version: 4.3.0
+Version: 4.3.1
Summary: Python multiprocessing fork with improvements and bugfixes
Home-page: https://github.com/celery/billiard
Author: R Oudkerk / Python Software Foundation
@@ -47,7 +47,7 @@
|build-status-lin| |build-status-win| |license| |wheel| |pyversion| |pyimp|
-:Version: 4.3.0
+:Version: 4.3.1
:Web: https://billiard.readthedocs.io
:Download: https://pypi.org/project/billiard/
:Source: https://github.com/celery/billiard/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/README.rst
new/billiard-4.3.1/README.rst
--- old/billiard-4.3.0/README.rst 2026-09-17 10:30:48.367270500 +0200
+++ new/billiard-4.3.1/README.rst 2026-10-05 08:31:02.569776300 +0200
@@ -4,7 +4,7 @@
|build-status-lin| |build-status-win| |license| |wheel| |pyversion| |pyimp|
-:Version: 4.3.0
+:Version: 4.3.1
:Web: https://billiard.readthedocs.io
:Download: https://pypi.org/project/billiard/
:Source: https://github.com/celery/billiard/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/billiard/__init__.py
new/billiard-4.3.1/billiard/__init__.py
--- old/billiard-4.3.0/billiard/__init__.py 2026-09-17 10:30:48.367686700
+0200
+++ new/billiard-4.3.1/billiard/__init__.py 2026-10-05 08:31:02.570095500
+0200
@@ -23,7 +23,7 @@
from . import context
-__version__ = '4.3.0'
+__version__ = '4.3.1'
__author__ = 'R Oudkerk / Python Software Foundation'
__author_email__ = '[email protected]'
__maintainer__ = 'Asif Saif Uddin'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/billiard/common.py
new/billiard-4.3.1/billiard/common.py
--- old/billiard-4.3.0/billiard/common.py 2026-09-17 08:52:08.068524000
+0200
+++ new/billiard-4.3.1/billiard/common.py 2026-10-05 08:12:40.525474500
+0200
@@ -74,6 +74,32 @@
_should_have_exited = [False]
+def terminate_handler_fired():
+ """Has this process's own terminate-signal handler already fired?
+
+ A `SystemExit` seen after that point was raised by
+ :func:`_shutdown_cleanup`, not by whatever the process happened to be
+ running, and means the process is on its way out.
+
+ Strictly about *this* process having already run the handler -- a
+ process that has been sent a terminate signal but has not yet handled
+ it answers False, which is why this is not named ``terminating()``.
+ """
+ return _should_have_exited[0]
+
+
+def clear_terminate_handler_fired():
+ """Forget that a terminate handler ran -- for use after :func:`fork`.
+
+ The flag is a module global and so is inherited across ``fork()``. A
+ fresh child has not run a terminate handler, whatever it inherited from
+ a parent that had, and must say so before it runs any user code: a
+ stale True makes :func:`_shutdown_cleanup` take its "something is very
+ wrong" :func:`os._exit` branch on the child's first terminate signal.
+ """
+ _should_have_exited[0] = False
+
+
def human_status(status):
if (status or 0) < 0:
try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/billiard/pool.py
new/billiard-4.3.1/billiard/pool.py
--- old/billiard-4.3.0/billiard/pool.py 2026-09-17 08:52:08.069891700 +0200
+++ new/billiard-4.3.1/billiard/pool.py 2026-10-05 08:12:40.525818000 +0200
@@ -27,7 +27,8 @@
from . import cpu_count, get_context
from . import util
from .common import (
- TERM_SIGNAL, human_status, pickle_loads, reset_signals, restart_state,
+ TERM_SIGNAL, clear_terminate_handler_fired, human_status, pickle_loads,
+ reset_signals, restart_state, terminate_handler_fired,
)
from .compat import get_errno, mem_rss, send_offset
from .einfo import ExceptionInfo
@@ -362,11 +363,50 @@
try:
result = (True, prepare_result(fun(*args, **kwargs)))
except BaseException as exc:
+ if terminate_handler_fired():
+ # Our own signal handler has already run in this
+ # process -- see common._shutdown_cleanup -- so we
+ # are unwinding towards exit and this job did not
+ # finish. There is no result to report: say
+ # nothing and let the process go, and the parent
+ # accounts for the job as lost with the worker.
+ # Reporting it would make an interrupted job look
+ # completed-with-failure, and a caller that
+ # acknowledges work only once it is done -- Celery
+ # with task_acks_late -- would then acknowledge a
+ # task nothing ever ran to the end.
+ #
+ # Deliberately not narrowed to SystemExit. If the
+ # job's own cleanup raises while that SystemExit
+ # unwinds -- a finally, or an __exit__ that fails,
+ # say a rollback on a connection the teardown has
+ # already closed -- the exception arriving here is
+ # that new error, and the job is every bit as
+ # unfinished (reported by @kratos0718 on #464).
+ #
+ # Known limitation, in the safer direction: a job
+ # that *swallows* the shutdown SystemExit leaves
+ # this flag set with the worker still alive, so a
+ # later job's own exception is reported as a lost
+ # worker rather than as itself. A spurious loss is
+ # redelivered rather than silently acked, and such
+ # a job has already defeated shutdown. Do not put
+ # the isinstance check back without first handling
+ # the cleanup-raises case above.
+ raise
result = (False, ExceptionInfo())
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
- # The worker was told to exit (e.g. SIGTERM from a
- # hard time limit): report the result, then leave
- # instead of re-entering the shared inqueue.
+ # Raised by the job itself: a result, and one the
+ # caller should see rather than a WorkerLostError
+ # (#427). Report it, then leave instead of
+ # re-entering the shared inqueue.
+ #
+ # Reached only when the flag is clear, so these
+ # really are the job's own: _shutdown_cleanup
+ # raises SystemExit and sets the flag first, and
+ # after_fork sets SIGINT to SIG_IGN, so a worker's
+ # KeyboardInterrupt can only have come from the
+ # job.
exit_exc = exc
try:
put((READY, (job, i, result, inqW_fd)))
@@ -420,6 +460,14 @@
return False
def after_fork(self):
+ # First, before self.initializer runs any user code: this child has
+ # not handled a terminate signal, whatever it inherited across the
+ # fork. Doing this here rather than as a side effect of
+ # reset_signals() below also closes the window where a signal
+ # arriving during a slow initializer (Celery's process_initializer)
+ # would be handled with the inherited flag still set.
+ clear_terminate_handler_fired()
+
if hasattr(self.inq, '_writer'):
self.inq._writer.close()
if hasattr(self.outq, '_reader'):
@@ -998,6 +1046,28 @@
max_memory_per_child=None,
enable_timeouts=False,
**kwargs):
+ # bool subclasses int; processes=True would silently spawn 1 worker,
+ # and timeout/soft_timeout/lost_worker_timeout=True would become 1s.
+ for name, value in (
+ ("processes", processes),
+ ("maxtasksperchild", maxtasksperchild),
+ ("timeout", timeout),
+ ("soft_timeout", soft_timeout),
+ ("lost_worker_timeout", lost_worker_timeout),
+ ("max_memory_per_child", max_memory_per_child),
+ ("max_restarts", max_restarts),
+ ("max_restart_freq", max_restart_freq),
+ ):
+ if isinstance(value, bool):
+ raise TypeError(
+ f"{name} must be an int or float, not bool (got {value!r})"
+ )
+ if processes is not None and processes < 1:
+ raise ValueError("Number of processes must be at least 1")
+ if maxtasksperchild is not None and (
+ not isinstance(maxtasksperchild, int) or maxtasksperchild < 1):
+ raise ValueError("maxtasksperchild must be a positive int or None")
+
self._ctx = context or get_context()
self.synack = synack
self._setup_queues()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/billiard/process.py
new/billiard-4.3.1/billiard/process.py
--- old/billiard-4.3.0/billiard/process.py 2026-09-17 08:52:08.070341000
+0200
+++ new/billiard-4.3.1/billiard/process.py 2026-10-05 08:12:40.526094200
+0200
@@ -88,6 +88,7 @@
self._config = _current_process._config.copy()
self._parent_pid = os.getpid()
self._popen = None
+ self._closed = False
self._target = target
self._args = tuple(args)
self._kwargs = dict(kwargs)
@@ -102,6 +103,10 @@
self._controlled_termination = False
+ def _check_closed(self):
+ if self._closed:
+ raise ValueError("process object is closed")
+
def run(self):
'''
Method to be run in sub-process; can be overridden in sub-class
@@ -113,6 +118,7 @@
'''
Start child process
'''
+ self._check_closed()
assert self._popen is None, 'cannot start a process twice'
assert self._parent_pid == os.getpid(), \
'can only start a process object created by current process'
@@ -122,13 +128,32 @@
_children.add(self)
def close(self):
+ '''
+ Close the Process object and release the resources it holds.
+
+ It is an error to call this method while the child is still running.
+ Other methods and properties then raise ValueError.
+ '''
if self._popen is not None:
+ # poll() also reports None when the child was already reaped by
+ # someone else (ECHILD, e.g. a SIGCHLD handler calling
+ # waitpid(-1)), so such a process can never be closed; this is
+ # the same in multiprocessing.
+ if self._popen.poll() is None:
+ raise ValueError(
+ "Cannot close a process while it is still running. "
+ "You should first call join() or terminate().")
self._popen.close()
+ self._popen = None
+ del self._sentinel
+ _children.discard(self)
+ self._closed = True
def terminate(self):
'''
Terminate process; sends SIGTERM signal or uses TerminateProcess()
'''
+ self._check_closed()
self._popen.terminate()
def terminate_controlled(self):
@@ -139,17 +164,20 @@
'''
Wait until child process terminates
'''
+ self._check_closed()
assert self._parent_pid == os.getpid(), 'can only join a child process'
assert self._popen is not None, 'can only join a started process'
res = self._popen.wait(timeout)
if res is not None:
_children.discard(self)
- self.close()
+ # only release the sentinel: callers still read exitcode after join
+ self._popen.close()
def is_alive(self):
'''
Return whether process is alive
'''
+ self._check_closed()
if self is _current_process:
return True
assert self._parent_pid == os.getpid(), 'can only test a child process'
@@ -203,6 +231,7 @@
'''
Return exit code of process or `None` if it has yet to stop
'''
+ self._check_closed()
if self._popen is None:
return self._popen
return self._popen.poll()
@@ -212,6 +241,7 @@
'''
Return identifier (PID) of process or `None` if it has yet to start
'''
+ self._check_closed()
if self is _current_process:
return os.getpid()
else:
@@ -225,6 +255,7 @@
Return a file descriptor (Unix) or handle (Windows) suitable for
waiting for process termination.
'''
+ self._check_closed()
try:
return self._sentinel
except AttributeError:
@@ -258,6 +289,8 @@
def __repr__(self):
if self is _current_process:
status = 'started'
+ elif self._closed:
+ status = 'closed'
elif self._parent_pid != os.getpid():
status = 'unknown'
elif self._popen is None:
@@ -375,6 +408,7 @@
self._name = 'MainProcess'
self._parent_pid = None
self._popen = None
+ self._closed = False
self._config = {'authkey': AuthenticationString(os.urandom(32)),
'semprefix': '/mp'}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/billiard/queues.py
new/billiard-4.3.1/billiard/queues.py
--- old/billiard-4.3.0/billiard/queues.py 2026-09-17 08:52:08.070471000
+0200
+++ new/billiard-4.3.1/billiard/queues.py 2026-10-05 08:12:40.526342400
+0200
@@ -83,7 +83,8 @@
self._poll = self._reader.poll
def put(self, obj, block=True, timeout=None):
- assert not self._closed
+ if self._closed:
+ raise ValueError(f"Queue {self!r} is closed")
if not self._sem.acquire(block, timeout):
raise Full
@@ -94,6 +95,8 @@
self._notempty.notify()
def get(self, block=True, timeout=None):
+ if self._closed:
+ raise ValueError(f"Queue {self!r} is closed")
if block and timeout is None:
with self._rlock:
res = self._recv_bytes()
@@ -147,7 +150,8 @@
def join_thread(self):
debug('Queue.join_thread()')
- assert self._closed
+ if not self._closed:
+ raise ValueError(f"Queue {self!r} not closed")
if self._jointhread:
self._jointhread()
@@ -304,7 +308,8 @@
self._cond, self._unfinished_tasks = state[-2:]
def put(self, obj, block=True, timeout=None):
- assert not self._closed
+ if self._closed:
+ raise ValueError(f"Queue {self!r} is closed")
if not self._sem.acquire(block, timeout):
raise Full
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/billiard.egg-info/PKG-INFO
new/billiard-4.3.1/billiard.egg-info/PKG-INFO
--- old/billiard-4.3.0/billiard.egg-info/PKG-INFO 2026-09-17
10:35:43.561669800 +0200
+++ new/billiard-4.3.1/billiard.egg-info/PKG-INFO 2026-10-05
08:37:27.336799600 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: billiard
-Version: 4.3.0
+Version: 4.3.1
Summary: Python multiprocessing fork with improvements and bugfixes
Home-page: https://github.com/celery/billiard
Author: R Oudkerk / Python Software Foundation
@@ -47,7 +47,7 @@
|build-status-lin| |build-status-win| |license| |wheel| |pyversion| |pyimp|
-:Version: 4.3.0
+:Version: 4.3.1
:Web: https://billiard.readthedocs.io
:Download: https://pypi.org/project/billiard/
:Source: https://github.com/celery/billiard/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/billiard.egg-info/SOURCES.txt
new/billiard-4.3.1/billiard.egg-info/SOURCES.txt
--- old/billiard-4.3.0/billiard.egg-info/SOURCES.txt 2026-09-17
10:35:43.569574000 +0200
+++ new/billiard-4.3.1/billiard.egg-info/SOURCES.txt 2026-10-05
08:37:27.344692700 +0200
@@ -64,6 +64,7 @@
t/integration/tests/__init__.py
t/integration/tests/test_multiprocessing.py
t/unit/__init__.py
+t/unit/conftest.py
t/unit/test_common.py
t/unit/test_compat.py
t/unit/test_context.py
@@ -71,6 +72,8 @@
t/unit/test_einfo.py
t/unit/test_package.py
t/unit/test_pool.py
+t/unit/test_process.py
+t/unit/test_queues.py
t/unit/test_spawn.py
t/unit/test_values.py
t/unit/test_win32.py
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/t/unit/conftest.py
new/billiard-4.3.1/t/unit/conftest.py
--- old/billiard-4.3.0/t/unit/conftest.py 1970-01-01 01:00:00.000000000
+0100
+++ new/billiard-4.3.1/t/unit/conftest.py 2026-10-05 08:12:40.526496000
+0200
@@ -0,0 +1,21 @@
+import pytest
+
+from billiard import common
+
+
[email protected](autouse=True)
+def reset_terminate_handler_fired():
+ """Keep ``common._should_have_exited`` from leaking between tests.
+
+ The flag is a module global, and any test that calls ``_shutdown_cleanup``
+ in the test process -- ``test_common.test_shutdown_handler`` does, with
+ ``sys.exit`` patched out -- leaves it True for the rest of the session.
+ Nothing resets it in-process, so a later test exercising the workloop
+ would silently take the shutdown branch and pass or fail for a reason
+ that has nothing to do with what it is testing.
+ """
+ previous = common._should_have_exited[0]
+ try:
+ yield
+ finally:
+ common._should_have_exited[0] = previous
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/t/unit/test_common.py
new/billiard-4.3.1/t/unit/test_common.py
--- old/billiard-4.3.0/t/unit/test_common.py 2026-09-17 08:52:08.072662000
+0200
+++ new/billiard-4.3.1/t/unit/test_common.py 2026-10-05 08:12:40.526700500
+0200
@@ -10,8 +10,10 @@
from billiard.common import (
_shutdown_cleanup,
+ clear_terminate_handler_fired,
reset_signals,
restart_state,
+ terminate_handler_fired,
)
@@ -79,6 +81,44 @@
yield GET, SET
[email protected]_win32()
+class test_terminate_handler_fired:
+ """The whole contract of the flag the workloop branches on.
+
+ ``_should_have_exited`` is restored around every test by the autouse
+ fixture in ``t/unit/conftest.py``; without it the first assertion here
+ would depend on which tests ran before.
+ """
+
+ def test_false_before_any_handler_runs(self):
+ assert terminate_handler_fired() is False
+
+ def test_true_after_shutdown_cleanup(self):
+ with patch('sys.exit'):
+ _shutdown_cleanup(signal.SIGTERM, Mock())
+ assert terminate_handler_fired() is True
+
+ def test_false_again_after_clear(self):
+ with patch('sys.exit'):
+ _shutdown_cleanup(signal.SIGTERM, Mock())
+ clear_terminate_handler_fired()
+ assert terminate_handler_fired() is False
+
+ def test_reset_signals_does_not_clear_it(self):
+ """Clearing is a separate, explicitly called concern.
+
+ A child clears the flag via clear_terminate_handler_fired() at the
+ top of Worker.after_fork(), not as a hidden side effect of
+ installing signal handlers. Downstream callers of reset_signals()
+ should not have shutdown state changed underneath them.
+ """
+ with patch('sys.exit'):
+ _shutdown_cleanup(signal.SIGTERM, Mock())
+ with patch('signal.signal'), patch('signal.getsignal'):
+ reset_signals()
+ assert terminate_handler_fired() is True
+
+
class test_restart_state:
def test_raises(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/t/unit/test_pool.py
new/billiard-4.3.1/t/unit/test_pool.py
--- old/billiard-4.3.0/t/unit/test_pool.py 2026-09-17 08:52:08.073204800
+0200
+++ new/billiard-4.3.1/t/unit/test_pool.py 2026-10-05 08:12:40.527043000
+0200
@@ -6,7 +6,10 @@
import billiard.pool
from billiard import get_context
-from billiard.exceptions import TimeLimitExceeded
+from billiard.einfo import ExceptionWithTraceback
+from billiard.exceptions import TimeLimitExceeded, WorkerLostError
+
+from t import skip
def func(x):
@@ -30,6 +33,39 @@
def raise_system_exit():
raise SystemExit(1)
+
+def announce_pid_then_sleep(marker):
+ """Publish the pid of the process actually running this job, then block.
+
+ Written to a temporary name and renamed into place: os.replace is atomic,
+ so the parent cannot observe the marker existing but still empty.
+
+ The sleep must outlast the parent's result.get() timeout by a wide margin.
+ If the two were comparable, a kill that was slow or ineffective would let
+ the job finish at about the moment the get gave up, and the test would
+ report whichever won rather than the behaviour it is checking.
+ """
+ with open(marker + '.tmp', 'w') as fh:
+ fh.write(str(os.getpid()))
+ os.replace(marker + '.tmp', marker)
+ time.sleep(300)
+
+def announce_pid_then_fail_in_cleanup(marker):
+ """As above, but the job's own cleanup raises on the way out.
+
+ A failing ``finally`` (or ``__exit__``) replaces the in-flight shutdown
+ SystemExit with its own exception, so what arrives at the workloop is a
+ RuntimeError. The job is no less unfinished for it.
+ """
+ try:
+ with open(marker + '.tmp', 'w') as fh:
+ fh.write(str(os.getpid()))
+ os.replace(marker + '.tmp', marker)
+ time.sleep(300)
+ finally:
+ raise RuntimeError('cleanup failed while shutting down')
+
+
class test_pool:
def test_memory_error_from_callback_propagates(self):
def callback(value):
@@ -94,6 +130,75 @@
pool.terminate()
pool.join()
+ @skip.if_win32()
+ @pytest.mark.parametrize('job', [
+ announce_pid_then_sleep,
+ announce_pid_then_fail_in_cleanup,
+ ], ids=['plain', 'cleanup_raises'])
+ def test_worker_terminated_mid_job_is_lost_not_completed(self, tmp_path,
+ job):
+ """A worker told to exit did not finish its job.
+
+ The SystemExit that terminates a worker is raised by billiard's own
+ signal handler, inside whatever the worker happened to be running. If
+ the worker reports that as the job's result, the job looks completed
+ (with a failure) rather than interrupted -- and a caller that
+ acknowledges work only once it is done, such as Celery with
+ task_acks_late, then acknowledges a job nothing ever ran to the end.
+
+ Run for two shapes of job. In the 'cleanup_raises' case the job's own
+ finally raises while that SystemExit unwinds, so the exception
+ reaching the workloop is a RuntimeError rather than the SystemExit --
+ which is why the guard cannot be narrowed to SystemExit.
+
+ Unix only, and deliberately so: on Windows os.kill maps to
+ TerminateProcess, which never runs the Python-level handler. The
+ worker would still die and the parent would still report the job
+ lost, so this test would pass there without terminate_handler_fired()
+ ever being consulted -- green, but not coverage.
+ """
+ pool = billiard.pool.Pool(1)
+ try:
+ marker = str(tmp_path / 'started')
+ result = pool.apply_async(job, (marker,))
+
+ # Take the pid from the child itself rather than from
+ # pool._pool[0]: that indexes the pool's current roster, which is
+ # only the process running this job as long as the pool has one
+ # worker and the Supervisor has had no cause to replace it.
+ deadline = time.monotonic() + 10
+ while time.monotonic() < deadline and not os.path.exists(marker):
+ time.sleep(0.1)
+ assert os.path.exists(marker), 'worker never started the job'
+ with open(marker) as fh:
+ worker_pid = int(fh.read())
+
+ os.kill(worker_pid, billiard.pool.TERM_SIGNAL)
+
+ # ApplyResult.get() re-raises ExceptionInfo.exception. The parent
+ # synthesises this failure locally in Pool.mark_as_worker_lost, so
+ # nothing is pickled and what surfaces is the
+ # ExceptionWithTraceback wrapper, whose .exc holds the real
+ # WorkerLostError -- `pytest.raises(WorkerLostError)` would NOT
+ # match here. A result that had travelled back through the queue
+ # would have been rebuilt by einfo.rebuild_exc into the bare
+ # exception, hence the tolerant unwrap below.
+ #
+ # Naming both types keeps a TimeoutError -- the parent never
+ # noticing the dead worker at all -- failing at the raises clause
+ # with an accurate message, instead of reaching the assert.
+ #
+ # Generous timeout: the parent notices the dead worker from its
+ # supervisor loop, so this bounds a failure rather than a wait.
+ with pytest.raises(
+ (WorkerLostError, ExceptionWithTraceback)) as excinfo:
+ result.get(timeout=30)
+ exc = getattr(excinfo.value, 'exc', excinfo.value)
+ assert isinstance(exc, WorkerLostError)
+ finally:
+ pool.terminate()
+ pool.join()
+
def test_hard_timeout_does_not_stall_pool(self):
pool = billiard.pool.Pool(1, timeout=1)
try:
@@ -184,3 +289,34 @@
# retrieved.
for i, result in enumerate(results):
assert result.get() == i * 2
+
+
+ def test_rejects_fewer_than_one_process(self):
+ for processes in (0, -1):
+ with pytest.raises(ValueError, match="at least 1"):
+ billiard.pool.Pool(processes=processes)
+
+ def test_rejects_non_positive_maxtasksperchild(self):
+ # Worker only asserted this, so it failed with a bare AssertionError,
+ # and under python -O the pool kept replacing workers that exited
+ # before running a task.
+ for maxtasksperchild in (0, -1, 1.5):
+ with pytest.raises(ValueError, match="maxtasksperchild"):
+ billiard.pool.Pool(processes=1,
+ maxtasksperchild=maxtasksperchild)
+
+ def test_rejects_bool_for_numeric_pool_args(self):
+ """bool subclasses int; processes=True must not silently spawn 1
worker."""
+ for kwargs in (
+ {"processes": True},
+ {"processes": False},
+ {"maxtasksperchild": True},
+ {"timeout": True},
+ {"soft_timeout": True},
+ {"lost_worker_timeout": True},
+ {"max_memory_per_child": True},
+ {"max_restarts": True},
+ {"max_restart_freq": True},
+ ):
+ with pytest.raises(TypeError, match="bool"):
+ billiard.pool.Pool(**kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/t/unit/test_process.py
new/billiard-4.3.1/t/unit/test_process.py
--- old/billiard-4.3.0/t/unit/test_process.py 1970-01-01 01:00:00.000000000
+0100
+++ new/billiard-4.3.1/t/unit/test_process.py 2026-10-05 08:12:40.527197000
+0200
@@ -0,0 +1,44 @@
+import time
+
+import pytest
+
+from billiard import Process
+
+
+class test_Process:
+
+ def test_close_while_running_raises(self):
+ # close() used to close the sentinel of a live child, after which
+ # join() failed with a TypeError and the child was never reaped.
+ p = Process(target=time.sleep, args=(0.5,))
+ p.start()
+ try:
+ with pytest.raises(ValueError, match='still running'):
+ p.close()
+ finally:
+ p.join(5)
+ assert p.exitcode == 0
+ p.close()
+
+ def test_join_keeps_the_process_usable(self):
+ # join() releases the sentinel itself, but the pool still reads
+ # exitcode and pid afterwards, so it must not close the object.
+ p = Process(target=time.sleep, args=(0,))
+ p.start()
+ p.join(5)
+ assert p.exitcode == 0
+ assert p.pid is not None
+ assert not p.is_alive()
+
+ def test_closed_process(self):
+ p = Process(target=time.sleep, args=(0,))
+ p.start()
+ p.join(5)
+ p.close()
+ assert 'closed' in repr(p)
+ for use in (p.start, p.join, p.is_alive, p.terminate,
+ lambda: p.exitcode, lambda: p.pid,
+ lambda: p.sentinel):
+ with pytest.raises(ValueError, match='process object is closed'):
+ use()
+ p.close() # closing twice is fine
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/billiard-4.3.0/t/unit/test_queues.py
new/billiard-4.3.1/t/unit/test_queues.py
--- old/billiard-4.3.0/t/unit/test_queues.py 1970-01-01 01:00:00.000000000
+0100
+++ new/billiard-4.3.1/t/unit/test_queues.py 2026-10-05 08:12:40.527333300
+0200
@@ -0,0 +1,36 @@
+import pytest
+
+from billiard import JoinableQueue, Queue
+
+
+class test_closed_queue:
+
+ @pytest.mark.parametrize('queue_type', [Queue, JoinableQueue])
+ def test_put_raises_value_error(self, queue_type):
+ # put() only asserted this, so under python -O the item was
+ # accepted and then lost by the feeder thread.
+ q = queue_type()
+ q.close()
+ with pytest.raises(ValueError, match='is closed'):
+ q.put(1)
+ with pytest.raises(ValueError, match='is closed'):
+ q.put_nowait(1)
+
+ @pytest.mark.parametrize('queue_type', [Queue, JoinableQueue])
+ def test_get_raises_value_error(self, queue_type):
+ q = queue_type()
+ q.close()
+ with pytest.raises(ValueError, match='is closed'):
+ q.get()
+ with pytest.raises(ValueError, match='is closed'):
+ q.get(timeout=0.1)
+ with pytest.raises(ValueError, match='is closed'):
+ q.get_nowait()
+
+ @pytest.mark.parametrize('queue_type', [Queue, JoinableQueue])
+ def test_join_thread_requires_closed_queue(self, queue_type):
+ q = queue_type()
+ with pytest.raises(ValueError, match='not closed'):
+ q.join_thread()
+ q.close()
+ q.join_thread()