coveragepy
coveragepy copied to clipboard
With asynchronous exception raised, thread.is_alive() unexpectedly blocks in Python 3.11 with coverage
Describe the bug
This program (minimized from Zulip’s TimeoutTestCase.test_timeout_warn) succeeds in Python 3.10 without coverage, 3.10 with coverage, and 3.11 without coverage, but it fails in 3.11 with coverage.
import ctypes
import threading
import time
thread = threading.Thread(target=lambda: time.sleep(1))
thread.daemon = True
thread.start()
time.sleep(0.1)
ctypes.pythonapi.PyThreadState_SetAsyncExc(
ctypes.c_ulong(thread.ident), ctypes.py_object(TimeoutError)
)
time.sleep(0.1)
thread.is_alive()
time.sleep(0.1)
assert thread.is_alive()
The program spawns sleep(1) in a thread, then uses the Python C API to raise an asynchronous exception within it, which has no immediate effect since it’s still sleeping. Both thread.is_alive() calls should immediately return True. But in Python 3.11 with coverage, it seems the first thread.is_alive() call unexpectedly blocks until the thread’s sleep(1) finishes before returning, so the second thread.is_alive() call returns False.
A git bisect of Python shows that this started failing with
- python/cpython#30633
To Reproduce
Run python3.11 -m coverage run test.py. I’m using the latest coverage==7.2.5 on Linux x86-64 (NixOS 23.05).
I have no idea what that change in CPython did, or how coverage affects it. If you have any more details about why this might be happening, I'd appreciate them.
I also don't understand why ctypes is needed to demonstrate the problem.
I’m not sure whether ctypes is necessary to demonstrate this problem, but I can say that ctypes is necessary to call PyThreadState_SetAsyncExc (“To prevent naive misuse, you must write your own C extension to call this.”), which is necessary to implement a timeout mechanism for an arbitrary function (see the func_timeout package, for example).
I don’t have more information about where the problem might be yet, but I’ll let you know if I find something.