Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 1 addition & 65 deletions Lib/concurrent/interpreters/_crossinterp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,7 @@ class ItemInterpreterDestroyed(Exception):
"""Raised when trying to get an item whose interpreter was destroyed."""


class classonly:
"""A non-data descriptor that makes a value only visible on the class.

This is like the "classmethod" builtin, but does not show up on
instances of the class. It may be used as a decorator.
"""

def __init__(self, value):
self.value = value
self.getter = classmethod(value).__get__
self.name = None

def __set_name__(self, cls, name):
if self.name is not None:
raise TypeError('already used')
self.name = name

def __get__(self, obj, cls):
if obj is not None:
raise AttributeError(self.name)
# called on the class
return self.getter(None, cls)


class UnboundItem:
"""Represents a cross-interpreter item no longer bound to an interpreter.

An item is unbound when the interpreter that added it to the
cross-interpreter container is destroyed.
"""

__slots__ = ()

@classonly
def singleton(cls, kind, module, name='UNBOUND'):
doc = cls.__doc__
if doc:
doc = doc.replace(
'cross-interpreter container', kind,
).replace(
'cross-interpreter', kind,
)
subclass = type(
f'Unbound{kind.capitalize()}Item',
(cls,),
{
"_MODULE": module,
"_NAME": name,
"__doc__": doc,
},
)
return object.__new__(subclass)

_MODULE = __name__
_NAME = 'UNBOUND'

def __new__(cls):
raise Exception(f'use {cls._MODULE}.{cls._NAME}')

def __repr__(self):
return f'{self._MODULE}.{self._NAME}'
# return f'interpreters._queues.UNBOUND'


UNBOUND = object.__new__(UnboundItem)
UNBOUND = object()
UNBOUND_ERROR = object()
UNBOUND_REMOVE = object()

Expand Down
5 changes: 1 addition & 4 deletions Lib/concurrent/interpreters/_queues.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
QueueError, QueueNotFoundError,
)
from ._crossinterp import (
UNBOUND_ERROR, UNBOUND_REMOVE,
UNBOUND, UNBOUND_ERROR, UNBOUND_REMOVE,
)

__all__ = [
Expand Down Expand Up @@ -46,9 +46,6 @@ class ItemInterpreterDestroyed(QueueError,
_PICKLED = 1


UNBOUND = _crossinterp.UnboundItem.singleton('queue', __name__)


def _serialize_unbound(unbound):
if unbound is UNBOUND:
unbound = _crossinterp.UNBOUND
Expand Down
5 changes: 1 addition & 4 deletions Lib/test/support/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
ChannelEmptyError, ChannelNotEmptyError, # noqa: F401
)
from concurrent.interpreters._crossinterp import (
UNBOUND_ERROR, UNBOUND_REMOVE,
UNBOUND, UNBOUND_ERROR, UNBOUND_REMOVE,
)


Expand All @@ -28,9 +28,6 @@ class ItemInterpreterDestroyed(ChannelError,
"""Raised from get() and get_nowait()."""


UNBOUND = _crossinterp.UnboundItem.singleton('queue', __name__)


def _serialize_unbound(unbound):
if unbound is UNBOUND:
unbound = _crossinterp.UNBOUND
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_concurrent_futures/test_interpreter_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,10 @@ def run(taskid, ready, blocker):
ready.get(timeout=1) # blocking
except interpreters.QueueEmpty:
pass
except queues.QueueEmpty:
# GH-142414: reloading the _queues module makes get to raise
# queues.QueueEmpty instead of interpreters.QueueEmpty.
pass
Comment on lines +430 to +433
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like for this to be in a separate PR with a better test case.

Copy link
Contributor Author

@note35 note35 Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is needed otherwise ./python Lib/test/test_runpy.py will run forever.

If a better test case is needed, we can introduce it in the same PR.

I haven’t yet figured out how the combination test causes this in a single test, I’ll need some more time to figure out that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't seem related to this PR. Can you reproduce the same infinite loop on main?

Copy link
Contributor Author

@note35 note35 Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the main branch has no issue because there’s no unbound reload (unbound is directly declared in queues) before this fix. The loop happens along with this fix.

Copy link
Contributor Author

@note35 note35 Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think twice.

The existing test suite should already provide sufficient coverage. Since test_interpreter_pool naturally fails with deadlock when the issue exists. So we don't need an additional dedicated test if the test modifications in the PR is considered a legitimate bug fix (not workarounds).

./python -m test test_interpreters test_concurrent_futures.test_interpreter_pool 
1. Before this fix: test_interpreters failed
2. After this fix: test_concurrent_futures.test_interpreter_pool failed without this catch
3. After this fix with the catch: all passed

else:
done += 1
pending -= done
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/test_interpreters/test_queues.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# Raise SkipTest if subinterpreters not supported.
_queues = import_helper.import_module('_interpqueues')
from concurrent import interpreters
from concurrent.futures import InterpreterPoolExecutor
from concurrent.interpreters import _queues as queues, _crossinterp
from .utils import _run_output, TestBase as _TestBase

Expand Down Expand Up @@ -93,6 +94,14 @@ def test_bind_release(self):
with self.assertRaises(queues.QueueError):
_queues.release(qid)

def test_interpreter_pool_executor_after_reload(self):
# Regression test for gh-142414 (KeyError in serialize_unbound).
importlib.reload(queues)
code = "import struct"
with InterpreterPoolExecutor(max_workers=1) as executor:
results = executor.map(exec, [code] * 1)
self.assertEqual(list(results), [None] * 1)


class QueueTests(TestBase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix spurious :exc:`KeyError` when :mod:`!concurrent.interpreters._queues` is reloaded after import.
Loading