Skip to content

Commit 25e69d6

Browse files
gh-145638: Add atomic signal wakeup state updates
1 parent 2423814 commit 25e69d6

12 files changed

Lines changed: 297 additions & 26 deletions

‎Doc/library/signal.rst‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,41 @@ The :mod:`!signal` module defines the following functions:
548548
.. availability:: Unix.
549549

550550

551-
.. function:: set_wakeup_fd(fd, *, warn_on_full_buffer=True)
551+
.. function:: get_wakeup()
552+
553+
Return a :class:`wakeup` snapshot of the current signal wakeup
554+
configuration without changing it. This can be used with
555+
:func:`set_wakeup_fd` to update the process-wide wakeup configuration only
556+
if another owner has not changed it.
557+
558+
When threads are enabled, this function can only be called from
559+
:ref:`the main thread of the main interpreter <signals-and-threads>`;
560+
attempting to call it from other threads will cause a :exc:`ValueError`
561+
exception to be raised.
562+
563+
.. versionadded:: 3.16
564+
565+
566+
.. class:: wakeup
567+
568+
An immutable snapshot of the signal wakeup configuration. It has these
569+
read-only attributes:
570+
571+
.. attribute:: wakeup.fd
572+
573+
The wakeup file descriptor, or ``-1`` if wakeups are disabled.
574+
575+
.. attribute:: wakeup.warn_on_full_buffer
576+
577+
Whether a full wakeup buffer reports warnings.
578+
579+
.. attribute:: wakeup.is_socket
580+
581+
Whether *fd* is used as a socket on Windows. This is always false on
582+
other platforms.
583+
584+
585+
.. function:: set_wakeup_fd(fd, *, warn_on_full_buffer=True, check_previous=None)
552586

553587
Set the wakeup file descriptor to *fd*. When a signal your program has
554588
registered a signal handler for is received, the signal number is written as
@@ -562,6 +596,11 @@ The :mod:`!signal` module defines the following functions:
562596
If not -1, *fd* must be non-blocking. It is up to the library to remove
563597
any bytes from *fd* before calling poll or select again.
564598

599+
If *check_previous* is a :class:`wakeup` snapshot, change the wakeup
600+
configuration only if it is still equal to the current configuration.
601+
Otherwise, raise :exc:`RuntimeError` without changing it. Pass ``None``
602+
(the default) to update unconditionally.
603+
565604
When threads are enabled, this function can only be called
566605
from :ref:`the main thread of the main interpreter <signals-and-threads>`;
567606
attempting to call it from other threads will cause a :exc:`ValueError`
@@ -594,6 +633,9 @@ The :mod:`!signal` module defines the following functions:
594633
.. versionchanged:: 3.7
595634
Added ``warn_on_full_buffer`` parameter.
596635

636+
.. versionchanged:: 3.16
637+
Added the ``check_previous`` parameter.
638+
597639
.. function:: siginterrupt(signalnum, flag)
598640

599641
Change system call restart behaviour: if *flag* is :const:`False`, system

‎Include/internal/pycore_global_objects_fini_generated.h‎

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

‎Include/internal/pycore_global_strings.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ struct _Py_global_strings {
374374
STRUCT_FOR_ID(cb_type)
375375
STRUCT_FOR_ID(certfile)
376376
STRUCT_FOR_ID(chain)
377+
STRUCT_FOR_ID(check_previous)
377378
STRUCT_FOR_ID(check_same_thread)
378379
STRUCT_FOR_ID(clamp)
379380
STRUCT_FOR_ID(clear)

‎Include/internal/pycore_runtime_init_generated.h‎

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

‎Include/internal/pycore_unicodeobject_generated.h‎

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

‎Lib/asyncio/proactor_events.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,11 +663,13 @@ def __init__(self, proactor):
663663
self._selector = proactor # convenient alias
664664
self._self_reading_future = None
665665
self._accept_futures = {} # socket file descriptor => Future
666+
self._wakeup = None
666667
proactor.set_loop(self)
667668
self._make_self_pipe()
668669
if threading.current_thread() is threading.main_thread():
669670
# wakeup fd can only be installed to a file descriptor from the main thread
670671
signal.set_wakeup_fd(self._csock.fileno())
672+
self._wakeup = signal.get_wakeup()
671673

672674
def _make_socket_transport(self, sock, protocol, waiter=None,
673675
extra=None, server=None, context=None):
@@ -715,8 +717,14 @@ def close(self):
715717
if self.is_closed():
716718
return
717719

718-
if threading.current_thread() is threading.main_thread():
719-
signal.set_wakeup_fd(-1)
720+
if (threading.current_thread() is threading.main_thread()
721+
and self._wakeup is not None):
722+
try:
723+
signal.set_wakeup_fd(-1, check_previous=self._wakeup)
724+
except RuntimeError:
725+
# Another loop or host replaced the process-global wakeup fd.
726+
pass
727+
self._wakeup = None
720728
# Call these methods before closing the event loop (before calling
721729
# BaseEventLoop.close), because they can schedule callbacks with
722730
# call_soon(), which is forbidden when the event loop is closed.

‎Lib/test/test_asyncio/test_proactor_events.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -768,19 +768,24 @@ def setUp(self):
768768

769769
with mock.patch('asyncio.proactor_events.socket.socketpair',
770770
return_value=(self.ssock, self.csock)):
771-
with mock.patch('signal.set_wakeup_fd'):
771+
with mock.patch('signal.set_wakeup_fd'), \
772+
mock.patch('signal.get_wakeup'):
772773
self.loop = BaseProactorEventLoop(self.proactor)
774+
self.loop._wakeup = None
773775
self.set_event_loop(self.loop)
774776

775777
@mock.patch('asyncio.proactor_events.socket.socketpair')
776778
def test_ctor(self, socketpair):
777779
ssock, csock = socketpair.return_value = (
778780
mock.Mock(), mock.Mock())
779-
with mock.patch('signal.set_wakeup_fd'):
781+
with mock.patch('signal.set_wakeup_fd'), \
782+
mock.patch('signal.get_wakeup'):
780783
loop = BaseProactorEventLoop(self.proactor)
781784
self.assertIs(loop._ssock, ssock)
782785
self.assertIs(loop._csock, csock)
783786
self.assertEqual(loop._internal_fds, 1)
787+
# The wakeup state is mocked in this unit test.
788+
loop._wakeup = None
784789
loop.close()
785790

786791
def test_close_self_pipe(self):

‎Lib/test/test_asyncio/test_windows_events.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,29 @@ def SIGINT_after_delay():
7878
thread.join()
7979

8080

81+
class ProactorWakeupOwnershipTests(WindowsEventsTestCase):
82+
83+
def test_close_preserves_newer_loop_wakeup(self):
84+
initial = signal.get_wakeup()
85+
first = asyncio.ProactorEventLoop()
86+
second = asyncio.ProactorEventLoop()
87+
try:
88+
newer_wakeup = signal.get_wakeup()
89+
self.assertEqual(newer_wakeup.fd, second._csock.fileno())
90+
91+
first.close()
92+
self.assertEqual(signal.get_wakeup(), newer_wakeup)
93+
finally:
94+
if not first.is_closed():
95+
first.close()
96+
if not second.is_closed():
97+
second.close()
98+
signal.set_wakeup_fd(
99+
initial.fd,
100+
warn_on_full_buffer=initial.warn_on_full_buffer,
101+
)
102+
103+
81104
class ProactorMultithreading(WindowsEventsTestCase):
82105
def test_run_from_nonmain_thread(self):
83106
finished = False

‎Lib/test/test_signal.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,48 @@ def test_set_wakeup_fd_socket_result(self):
288288
self.assertEqual(signal.set_wakeup_fd(-1), fd2)
289289
self.assertEqual(signal.set_wakeup_fd(-1), -1)
290290

291+
@unittest.skipUnless(support.has_socket_support, "needs working sockets.")
292+
def test_wakeup_state_and_conditional_update(self):
293+
first, second = socket.socketpair()
294+
self.addCleanup(first.close)
295+
self.addCleanup(second.close)
296+
first.setblocking(False)
297+
second.setblocking(False)
298+
self.addCleanup(signal.set_wakeup_fd, -1)
299+
300+
initial_state = signal.get_wakeup()
301+
self.assertEqual(initial_state.fd, -1)
302+
self.assertFalse(initial_state.is_socket)
303+
signal.set_wakeup_fd(first.fileno(), warn_on_full_buffer=False)
304+
first_state = signal.get_wakeup()
305+
self.assertEqual(first_state.fd, first.fileno())
306+
self.assertFalse(first_state.warn_on_full_buffer)
307+
self.assertEqual(first_state.is_socket, sys.platform == "win32")
308+
309+
signal.set_wakeup_fd(
310+
first.fileno(),
311+
warn_on_full_buffer=True,
312+
check_previous=first_state,
313+
)
314+
changed_state = signal.get_wakeup()
315+
self.assertEqual(changed_state.fd, first.fileno())
316+
self.assertTrue(changed_state.warn_on_full_buffer)
317+
318+
with self.assertRaisesRegex(RuntimeError,
319+
"signal wakeup configuration changed"):
320+
signal.set_wakeup_fd(second.fileno(), check_previous=first_state)
321+
self.assertEqual(signal.get_wakeup(), changed_state)
322+
323+
signal.set_wakeup_fd(second.fileno(), check_previous=changed_state)
324+
second_state = signal.get_wakeup()
325+
self.assertEqual(second_state.fd, second.fileno())
326+
self.assertTrue(second_state.warn_on_full_buffer)
327+
328+
with self.assertRaisesRegex(RuntimeError,
329+
"signal wakeup configuration changed"):
330+
signal.set_wakeup_fd(-1, check_previous=first_state)
331+
self.assertEqual(signal.get_wakeup(), second_state)
332+
291333
# On Windows, files are always blocking and Windows does not provide a
292334
# function to test if a socket is in non-blocking mode.
293335
@unittest.skipIf(sys.platform == "win32", "tests specific to POSIX")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add :func:`signal.get_wakeup` and conditional ``check_previous`` support
2+
to :func:`signal.set_wakeup_fd`, allowing cooperating event loops and
3+
hosts to avoid overwriting each other's signal wakeup configuration.

0 commit comments

Comments
 (0)