From 34af479862852f34c85df87dcacac535bab32783 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Fri, 23 Oct 2026 11:40:53 -0400 Subject: [PATCH] fix(examples): discard a SIGALRM still pending as _deadline puts the handler back (#2090) _deadline.__exit__ cancelled the alarm and restored the previous handler directly, so on GraalPy a SIGALRM that fired just before the cancel could be delivered to SIG_DFL afterwards and end the worker, as in #2084. Ignoring the signal first discards it. The new test holds it pending with pthread_sigmask. --- examples/generators/options.py | 5 +++ ...tion_generator_deadline_scale_1428_unit.py | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/examples/generators/options.py b/examples/generators/options.py index d1afbb50f2..084e2d276a 100644 --- a/examples/generators/options.py +++ b/examples/generators/options.py @@ -1927,6 +1927,11 @@ def __exit__(self, *exc_info: 'Any') -> 'None': # Cancel before restoring, so an alarm firing between the two # cannot be delivered to whatever handler was installed before. signal.alarm(0) + # Cancelling does not withdraw an alarm that has fired but is not yet + # delivered, which on GraalPy it can still be: delivered once the + # previous handler is back -- ``SIG_DFL``, under pytest -- it ends the + # worker (#2084, #2090). Ignoring the signal discards a pending one. + signal.signal(signal.SIGALRM, signal.SIG_IGN) signal.signal(signal.SIGALRM, self.previous) self.armed = False diff --git a/tests/protocols/test_option_generator_deadline_scale_1428_unit.py b/tests/protocols/test_option_generator_deadline_scale_1428_unit.py index 8cf5e7235d..448a0e4c29 100644 --- a/tests/protocols/test_option_generator_deadline_scale_1428_unit.py +++ b/tests/protocols/test_option_generator_deadline_scale_1428_unit.py @@ -528,6 +528,41 @@ def firing_alarm(seconds: int) -> int: {'raised': result, 'pending': pending, 'restored': handler is self.handler}, {'raised': None, 'pending': 0, 'restored': True}) + @unittest.skipUnless(hasattr(signal, 'pthread_sigmask'), 'needs signal.pthread_sigmask') + def test_a_firing_still_pending_as_the_deadline_cancels_is_discarded(self) -> None: + """#2090: a firing not yet delivered by the restore is never delivered. + + GraalPy delivers a signal asynchronously, so one that fired just before the + cancel can still be pending once :meth:`_deadline.__exit__` has put the + previous handler back -- ``SIG_DFL``, under pytest, which ends the worker + (#2084). Blocking the signal stands in for that delay: it is sent from + inside the cancel and stays pending until the ``with`` has been left, and + the handler put back must not then receive it. + """ + real_alarm = signal.alarm + handled = [] # type: list[int] + signal.signal(signal.SIGALRM, lambda signum, frame: handled.append(signum)) + sent = [] # type: list[int] + + def firing_alarm(seconds: int) -> int: + if not seconds and not sent: + sent.append(signal.SIGALRM) + signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGALRM}) + signal.pthread_kill(threading.get_ident(), signal.SIGALRM) + return real_alarm(seconds) + + try: + with mock.patch.object(signal, 'alarm', firing_alarm): + with self.options._deadline(5): # pylint: disable=protected-access + pass + finally: + # Unblocked, a signal still pending is delivered before this returns. + signal.pthread_sigmask(signal.SIG_UNBLOCK, {signal.SIGALRM}) + pending = real_alarm(0) + signal.signal(signal.SIGALRM, self.handler) + self.assertEqual({'sent': sent, 'handled': handled, 'pending': pending}, + {'sent': [signal.SIGALRM], 'handled': [], 'pending': 0}) + if __name__ == '__main__': unittest.main()