Skip to content

Commit f9b7c63

Browse files
gh-144748: Make EINTR retry paths honor PyThreadState_SetAsyncExc
1 parent d1505b5 commit f9b7c63

3 files changed

Lines changed: 45 additions & 0 deletions

File tree

Lib/test/test_threading.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import textwrap
2323
import traceback
2424
import warnings
25+
import ctypes
2526

2627
from unittest import mock
2728
from test import lock_tests
@@ -412,6 +413,39 @@ def run(self):
412413
t.join()
413414
# else the thread is still running, and we have no way to kill it
414415

416+
@cpython_only
417+
@unittest.skipIf(not hasattr(signal, "pthread_kill"), "requires pthread_kill (Unix only)")
418+
def test_PyThreadState_SetAsyncExc_interrupts_sleep(self):
419+
set_async_exc = ctypes.pythonapi.PyThreadState_SetAsyncExc
420+
set_async_exc.argtypes = (ctypes.c_ulong, ctypes.py_object)
421+
422+
class AsyncExc(Exception):
423+
pass
424+
425+
exception = ctypes.py_object(AsyncExc)
426+
signal.signal(signal.SIGUSR1, lambda *_: None)
427+
worker_started = threading.Event()
428+
worker_finished = threading.Event()
429+
430+
def worker():
431+
tid = threading.get_ident()
432+
worker_started.set()
433+
try:
434+
time.sleep(10) # blocked in EINTR retry path
435+
except AsyncExc:
436+
worker_finished.set()
437+
438+
t = threading.Thread(target=worker)
439+
t.start()
440+
worker_started.wait()
441+
time.sleep(0.2)
442+
result = set_async_exc(t.ident, exception)
443+
self.assertEqual(result, 1)
444+
signal.pthread_kill(t.ident, signal.SIGUSR1)
445+
worker_finished.wait(timeout=3)
446+
self.assertTrue(worker_finished.is_set())
447+
t.join(timeout=3)
448+
415449
def test_limbo_cleanup(self):
416450
# Issue 7481: Failure to start thread should cleanup the limbo map.
417451
def fail_new_thread(*args, **kwargs):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix PyThreadState_SetAsyncExc not interrupting threads blocked in EINTR
2+
retry paths such as time.sleep().

Python/ceval_gil.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,15 @@ _PyEval_MakePendingCalls(PyThreadState *tstate)
10501050
return res;
10511051
}
10521052

1053+
/* Check for async exceptions (PyThreadState_SetAsyncExc) */
1054+
if (tstate->async_exc != NULL) {
1055+
PyObject *exc = tstate->async_exc;
1056+
tstate->async_exc = NULL;
1057+
PyErr_SetNone(exc);
1058+
Py_DECREF(exc);
1059+
return -1;
1060+
}
1061+
10531062
return 0;
10541063
}
10551064

0 commit comments

Comments
 (0)