mirror of
https://github.com/python/cpython.git
synced 2025-10-20 08:23:47 +00:00

Fix race condition in test_external_inspection thread status tests The tests test_thread_status_detection and test_thread_status_gil_detection had a race condition where the test could sample thread status between when the sleeper thread sends its "ready" message and when it actually calls time.sleep(). This caused intermittent test failures where the sleeper thread would show as running (status=0) instead of idle (status=1 or 2). The fix moves the thread status collection inside the retry loop and specifically waits for the expected thread states before proceeding with assertions. The retry loop now continues until: - The sleeper thread shows as idle (status=1 for CPU mode, status=2 for GIL mode) - The busy thread shows as running (status=0) - Both thread IDs are found in the status collection This ensures the test waits for threads to settle into their expected states before making assertions, eliminating the race condition.
1906 lines
75 KiB
Python
1906 lines
75 KiB
Python
import unittest
|
|
import os
|
|
import textwrap
|
|
import importlib
|
|
import sys
|
|
import socket
|
|
import threading
|
|
import time
|
|
from asyncio import staggered, taskgroups, base_events, tasks
|
|
from unittest.mock import ANY
|
|
from test.support import (
|
|
os_helper,
|
|
SHORT_TIMEOUT,
|
|
busy_retry,
|
|
requires_gil_enabled,
|
|
)
|
|
from test.support.script_helper import make_script
|
|
from test.support.socket_helper import find_unused_port
|
|
|
|
import subprocess
|
|
|
|
# Profiling mode constants
|
|
PROFILING_MODE_WALL = 0
|
|
PROFILING_MODE_CPU = 1
|
|
PROFILING_MODE_GIL = 2
|
|
|
|
try:
|
|
from concurrent import interpreters
|
|
except ImportError:
|
|
interpreters = None
|
|
|
|
PROCESS_VM_READV_SUPPORTED = False
|
|
|
|
try:
|
|
from _remote_debugging import PROCESS_VM_READV_SUPPORTED
|
|
from _remote_debugging import RemoteUnwinder
|
|
from _remote_debugging import FrameInfo, CoroInfo, TaskInfo
|
|
except ImportError:
|
|
raise unittest.SkipTest(
|
|
"Test only runs when _remote_debugging is available"
|
|
)
|
|
|
|
|
|
def _make_test_script(script_dir, script_basename, source):
|
|
to_return = make_script(script_dir, script_basename, source)
|
|
importlib.invalidate_caches()
|
|
return to_return
|
|
|
|
|
|
skip_if_not_supported = unittest.skipIf(
|
|
(
|
|
sys.platform != "darwin"
|
|
and sys.platform != "linux"
|
|
and sys.platform != "win32"
|
|
),
|
|
"Test only runs on Linux, Windows and MacOS",
|
|
)
|
|
|
|
|
|
def requires_subinterpreters(meth):
|
|
"""Decorator to skip a test if subinterpreters are not supported."""
|
|
return unittest.skipIf(interpreters is None,
|
|
'subinterpreters required')(meth)
|
|
|
|
|
|
def get_stack_trace(pid):
|
|
unwinder = RemoteUnwinder(pid, all_threads=True, debug=True)
|
|
return unwinder.get_stack_trace()
|
|
|
|
|
|
def get_async_stack_trace(pid):
|
|
unwinder = RemoteUnwinder(pid, debug=True)
|
|
return unwinder.get_async_stack_trace()
|
|
|
|
|
|
def get_all_awaited_by(pid):
|
|
unwinder = RemoteUnwinder(pid, debug=True)
|
|
return unwinder.get_all_awaited_by()
|
|
|
|
|
|
class TestGetStackTrace(unittest.TestCase):
|
|
maxDiff = None
|
|
|
|
@skip_if_not_supported
|
|
@unittest.skipIf(
|
|
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
|
"Test only runs on Linux with process_vm_readv support",
|
|
)
|
|
def test_remote_stack_trace(self):
|
|
# Spawn a process with some realistic Python code
|
|
port = find_unused_port()
|
|
script = textwrap.dedent(
|
|
f"""\
|
|
import time, sys, socket, threading
|
|
# Connect to the test process
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
def bar():
|
|
for x in range(100):
|
|
if x == 50:
|
|
baz()
|
|
|
|
def baz():
|
|
foo()
|
|
|
|
def foo():
|
|
sock.sendall(b"ready:thread\\n"); time.sleep(10_000) # same line number
|
|
|
|
t = threading.Thread(target=bar)
|
|
t.start()
|
|
sock.sendall(b"ready:main\\n"); t.join() # same line number
|
|
"""
|
|
)
|
|
stack_trace = None
|
|
with os_helper.temp_dir() as work_dir:
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
|
|
# Create a socket server to communicate with the target process
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(1)
|
|
|
|
script_name = _make_test_script(script_dir, "script", script)
|
|
client_socket = None
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
client_socket, _ = server_socket.accept()
|
|
server_socket.close()
|
|
response = b""
|
|
while (
|
|
b"ready:main" not in response
|
|
or b"ready:thread" not in response
|
|
):
|
|
response += client_socket.recv(1024)
|
|
stack_trace = get_stack_trace(p.pid)
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
finally:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.kill()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
thread_expected_stack_trace = [
|
|
FrameInfo([script_name, 15, "foo"]),
|
|
FrameInfo([script_name, 12, "baz"]),
|
|
FrameInfo([script_name, 9, "bar"]),
|
|
FrameInfo([threading.__file__, ANY, "Thread.run"]),
|
|
]
|
|
# Is possible that there are more threads, so we check that the
|
|
# expected stack traces are in the result (looking at you Windows!)
|
|
found_expected_stack = False
|
|
for interpreter_info in stack_trace:
|
|
for thread_info in interpreter_info.threads:
|
|
if thread_info.frame_info == thread_expected_stack_trace:
|
|
found_expected_stack = True
|
|
break
|
|
if found_expected_stack:
|
|
break
|
|
self.assertTrue(found_expected_stack, "Expected thread stack trace not found")
|
|
|
|
# Check that the main thread stack trace is in the result
|
|
frame = FrameInfo([script_name, 19, "<module>"])
|
|
main_thread_found = False
|
|
for interpreter_info in stack_trace:
|
|
for thread_info in interpreter_info.threads:
|
|
if frame in thread_info.frame_info:
|
|
main_thread_found = True
|
|
break
|
|
if main_thread_found:
|
|
break
|
|
self.assertTrue(main_thread_found, "Main thread stack trace not found in result")
|
|
|
|
@skip_if_not_supported
|
|
@unittest.skipIf(
|
|
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
|
"Test only runs on Linux with process_vm_readv support",
|
|
)
|
|
def test_async_remote_stack_trace(self):
|
|
# Spawn a process with some realistic Python code
|
|
port = find_unused_port()
|
|
script = textwrap.dedent(
|
|
f"""\
|
|
import asyncio
|
|
import time
|
|
import sys
|
|
import socket
|
|
# Connect to the test process
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
def c5():
|
|
sock.sendall(b"ready"); time.sleep(10_000) # same line number
|
|
|
|
async def c4():
|
|
await asyncio.sleep(0)
|
|
c5()
|
|
|
|
async def c3():
|
|
await c4()
|
|
|
|
async def c2():
|
|
await c3()
|
|
|
|
async def c1(task):
|
|
await task
|
|
|
|
async def main():
|
|
async with asyncio.TaskGroup() as tg:
|
|
task = tg.create_task(c2(), name="c2_root")
|
|
tg.create_task(c1(task), name="sub_main_1")
|
|
tg.create_task(c1(task), name="sub_main_2")
|
|
|
|
def new_eager_loop():
|
|
loop = asyncio.new_event_loop()
|
|
eager_task_factory = asyncio.create_eager_task_factory(
|
|
asyncio.Task)
|
|
loop.set_task_factory(eager_task_factory)
|
|
return loop
|
|
|
|
asyncio.run(main(), loop_factory={{TASK_FACTORY}})
|
|
"""
|
|
)
|
|
stack_trace = None
|
|
for task_factory_variant in "asyncio.new_event_loop", "new_eager_loop":
|
|
with (
|
|
self.subTest(task_factory_variant=task_factory_variant),
|
|
os_helper.temp_dir() as work_dir,
|
|
):
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
server_socket = socket.socket(
|
|
socket.AF_INET, socket.SOCK_STREAM
|
|
)
|
|
server_socket.setsockopt(
|
|
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
|
|
)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(1)
|
|
script_name = _make_test_script(
|
|
script_dir,
|
|
"script",
|
|
script.format(TASK_FACTORY=task_factory_variant),
|
|
)
|
|
client_socket = None
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
client_socket, _ = server_socket.accept()
|
|
server_socket.close()
|
|
response = client_socket.recv(1024)
|
|
self.assertEqual(response, b"ready")
|
|
stack_trace = get_async_stack_trace(p.pid)
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
finally:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.kill()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
# First check all the tasks are present
|
|
tasks_names = [
|
|
task.task_name for task in stack_trace[0].awaited_by
|
|
]
|
|
for task_name in ["c2_root", "sub_main_1", "sub_main_2"]:
|
|
self.assertIn(task_name, tasks_names)
|
|
|
|
# Now ensure that the awaited_by_relationships are correct
|
|
id_to_task = {
|
|
task.task_id: task for task in stack_trace[0].awaited_by
|
|
}
|
|
task_name_to_awaited_by = {
|
|
task.task_name: set(
|
|
id_to_task[awaited.task_name].task_name
|
|
for awaited in task.awaited_by
|
|
)
|
|
for task in stack_trace[0].awaited_by
|
|
}
|
|
self.assertEqual(
|
|
task_name_to_awaited_by,
|
|
{
|
|
"c2_root": {"Task-1", "sub_main_1", "sub_main_2"},
|
|
"Task-1": set(),
|
|
"sub_main_1": {"Task-1"},
|
|
"sub_main_2": {"Task-1"},
|
|
},
|
|
)
|
|
|
|
# Now ensure that the coroutine stacks are correct
|
|
coroutine_stacks = {
|
|
task.task_name: sorted(
|
|
tuple(tuple(frame) for frame in coro.call_stack)
|
|
for coro in task.coroutine_stack
|
|
)
|
|
for task in stack_trace[0].awaited_by
|
|
}
|
|
self.assertEqual(
|
|
coroutine_stacks,
|
|
{
|
|
"Task-1": [
|
|
(
|
|
tuple(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup._aexit",
|
|
]
|
|
),
|
|
tuple(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup.__aexit__",
|
|
]
|
|
),
|
|
tuple([script_name, 26, "main"]),
|
|
)
|
|
],
|
|
"c2_root": [
|
|
(
|
|
tuple([script_name, 10, "c5"]),
|
|
tuple([script_name, 14, "c4"]),
|
|
tuple([script_name, 17, "c3"]),
|
|
tuple([script_name, 20, "c2"]),
|
|
)
|
|
],
|
|
"sub_main_1": [(tuple([script_name, 23, "c1"]),)],
|
|
"sub_main_2": [(tuple([script_name, 23, "c1"]),)],
|
|
},
|
|
)
|
|
|
|
# Now ensure the coroutine stacks for the awaited_by relationships are correct.
|
|
awaited_by_coroutine_stacks = {
|
|
task.task_name: sorted(
|
|
(
|
|
id_to_task[coro.task_name].task_name,
|
|
tuple(tuple(frame) for frame in coro.call_stack),
|
|
)
|
|
for coro in task.awaited_by
|
|
)
|
|
for task in stack_trace[0].awaited_by
|
|
}
|
|
self.assertEqual(
|
|
awaited_by_coroutine_stacks,
|
|
{
|
|
"Task-1": [],
|
|
"c2_root": [
|
|
(
|
|
"Task-1",
|
|
(
|
|
tuple(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup._aexit",
|
|
]
|
|
),
|
|
tuple(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup.__aexit__",
|
|
]
|
|
),
|
|
tuple([script_name, 26, "main"]),
|
|
),
|
|
),
|
|
("sub_main_1", (tuple([script_name, 23, "c1"]),)),
|
|
("sub_main_2", (tuple([script_name, 23, "c1"]),)),
|
|
],
|
|
"sub_main_1": [
|
|
(
|
|
"Task-1",
|
|
(
|
|
tuple(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup._aexit",
|
|
]
|
|
),
|
|
tuple(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup.__aexit__",
|
|
]
|
|
),
|
|
tuple([script_name, 26, "main"]),
|
|
),
|
|
)
|
|
],
|
|
"sub_main_2": [
|
|
(
|
|
"Task-1",
|
|
(
|
|
tuple(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup._aexit",
|
|
]
|
|
),
|
|
tuple(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup.__aexit__",
|
|
]
|
|
),
|
|
tuple([script_name, 26, "main"]),
|
|
),
|
|
)
|
|
],
|
|
},
|
|
)
|
|
|
|
@skip_if_not_supported
|
|
@unittest.skipIf(
|
|
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
|
"Test only runs on Linux with process_vm_readv support",
|
|
)
|
|
def test_asyncgen_remote_stack_trace(self):
|
|
# Spawn a process with some realistic Python code
|
|
port = find_unused_port()
|
|
script = textwrap.dedent(
|
|
f"""\
|
|
import asyncio
|
|
import time
|
|
import sys
|
|
import socket
|
|
# Connect to the test process
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
async def gen_nested_call():
|
|
sock.sendall(b"ready"); time.sleep(10_000) # same line number
|
|
|
|
async def gen():
|
|
for num in range(2):
|
|
yield num
|
|
if num == 1:
|
|
await gen_nested_call()
|
|
|
|
async def main():
|
|
async for el in gen():
|
|
pass
|
|
|
|
asyncio.run(main())
|
|
"""
|
|
)
|
|
stack_trace = None
|
|
with os_helper.temp_dir() as work_dir:
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
# Create a socket server to communicate with the target process
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(1)
|
|
script_name = _make_test_script(script_dir, "script", script)
|
|
client_socket = None
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
client_socket, _ = server_socket.accept()
|
|
server_socket.close()
|
|
response = client_socket.recv(1024)
|
|
self.assertEqual(response, b"ready")
|
|
stack_trace = get_async_stack_trace(p.pid)
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
finally:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.kill()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
# For this simple asyncgen test, we only expect one task with the full coroutine stack
|
|
self.assertEqual(len(stack_trace[0].awaited_by), 1)
|
|
task = stack_trace[0].awaited_by[0]
|
|
self.assertEqual(task.task_name, "Task-1")
|
|
|
|
# Check the coroutine stack - based on actual output, only shows main
|
|
coroutine_stack = sorted(
|
|
tuple(tuple(frame) for frame in coro.call_stack)
|
|
for coro in task.coroutine_stack
|
|
)
|
|
self.assertEqual(
|
|
coroutine_stack,
|
|
[
|
|
(
|
|
tuple([script_name, 10, "gen_nested_call"]),
|
|
tuple([script_name, 16, "gen"]),
|
|
tuple([script_name, 19, "main"]),
|
|
)
|
|
],
|
|
)
|
|
|
|
# No awaited_by relationships expected for this simple case
|
|
self.assertEqual(task.awaited_by, [])
|
|
|
|
@skip_if_not_supported
|
|
@unittest.skipIf(
|
|
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
|
"Test only runs on Linux with process_vm_readv support",
|
|
)
|
|
def test_async_gather_remote_stack_trace(self):
|
|
# Spawn a process with some realistic Python code
|
|
port = find_unused_port()
|
|
script = textwrap.dedent(
|
|
f"""\
|
|
import asyncio
|
|
import time
|
|
import sys
|
|
import socket
|
|
# Connect to the test process
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
async def deep():
|
|
await asyncio.sleep(0)
|
|
sock.sendall(b"ready"); time.sleep(10_000) # same line number
|
|
|
|
async def c1():
|
|
await asyncio.sleep(0)
|
|
await deep()
|
|
|
|
async def c2():
|
|
await asyncio.sleep(0)
|
|
|
|
async def main():
|
|
await asyncio.gather(c1(), c2())
|
|
|
|
asyncio.run(main())
|
|
"""
|
|
)
|
|
stack_trace = None
|
|
with os_helper.temp_dir() as work_dir:
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
# Create a socket server to communicate with the target process
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(1)
|
|
script_name = _make_test_script(script_dir, "script", script)
|
|
client_socket = None
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
client_socket, _ = server_socket.accept()
|
|
server_socket.close()
|
|
response = client_socket.recv(1024)
|
|
self.assertEqual(response, b"ready")
|
|
stack_trace = get_async_stack_trace(p.pid)
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
finally:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.kill()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
# First check all the tasks are present
|
|
tasks_names = [
|
|
task.task_name for task in stack_trace[0].awaited_by
|
|
]
|
|
for task_name in ["Task-1", "Task-2"]:
|
|
self.assertIn(task_name, tasks_names)
|
|
|
|
# Now ensure that the awaited_by_relationships are correct
|
|
id_to_task = {
|
|
task.task_id: task for task in stack_trace[0].awaited_by
|
|
}
|
|
task_name_to_awaited_by = {
|
|
task.task_name: set(
|
|
id_to_task[awaited.task_name].task_name
|
|
for awaited in task.awaited_by
|
|
)
|
|
for task in stack_trace[0].awaited_by
|
|
}
|
|
self.assertEqual(
|
|
task_name_to_awaited_by,
|
|
{
|
|
"Task-1": set(),
|
|
"Task-2": {"Task-1"},
|
|
},
|
|
)
|
|
|
|
# Now ensure that the coroutine stacks are correct
|
|
coroutine_stacks = {
|
|
task.task_name: sorted(
|
|
tuple(tuple(frame) for frame in coro.call_stack)
|
|
for coro in task.coroutine_stack
|
|
)
|
|
for task in stack_trace[0].awaited_by
|
|
}
|
|
self.assertEqual(
|
|
coroutine_stacks,
|
|
{
|
|
"Task-1": [(tuple([script_name, 21, "main"]),)],
|
|
"Task-2": [
|
|
(
|
|
tuple([script_name, 11, "deep"]),
|
|
tuple([script_name, 15, "c1"]),
|
|
)
|
|
],
|
|
},
|
|
)
|
|
|
|
# Now ensure the coroutine stacks for the awaited_by relationships are correct.
|
|
awaited_by_coroutine_stacks = {
|
|
task.task_name: sorted(
|
|
(
|
|
id_to_task[coro.task_name].task_name,
|
|
tuple(tuple(frame) for frame in coro.call_stack),
|
|
)
|
|
for coro in task.awaited_by
|
|
)
|
|
for task in stack_trace[0].awaited_by
|
|
}
|
|
self.assertEqual(
|
|
awaited_by_coroutine_stacks,
|
|
{
|
|
"Task-1": [],
|
|
"Task-2": [
|
|
("Task-1", (tuple([script_name, 21, "main"]),))
|
|
],
|
|
},
|
|
)
|
|
|
|
@skip_if_not_supported
|
|
@unittest.skipIf(
|
|
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
|
"Test only runs on Linux with process_vm_readv support",
|
|
)
|
|
def test_async_staggered_race_remote_stack_trace(self):
|
|
# Spawn a process with some realistic Python code
|
|
port = find_unused_port()
|
|
script = textwrap.dedent(
|
|
f"""\
|
|
import asyncio.staggered
|
|
import time
|
|
import sys
|
|
import socket
|
|
# Connect to the test process
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
async def deep():
|
|
await asyncio.sleep(0)
|
|
sock.sendall(b"ready"); time.sleep(10_000) # same line number
|
|
|
|
async def c1():
|
|
await asyncio.sleep(0)
|
|
await deep()
|
|
|
|
async def c2():
|
|
await asyncio.sleep(10_000)
|
|
|
|
async def main():
|
|
await asyncio.staggered.staggered_race(
|
|
[c1, c2],
|
|
delay=None,
|
|
)
|
|
|
|
asyncio.run(main())
|
|
"""
|
|
)
|
|
stack_trace = None
|
|
with os_helper.temp_dir() as work_dir:
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
# Create a socket server to communicate with the target process
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(1)
|
|
script_name = _make_test_script(script_dir, "script", script)
|
|
client_socket = None
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
client_socket, _ = server_socket.accept()
|
|
server_socket.close()
|
|
response = client_socket.recv(1024)
|
|
self.assertEqual(response, b"ready")
|
|
stack_trace = get_async_stack_trace(p.pid)
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
finally:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.kill()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
# First check all the tasks are present
|
|
tasks_names = [
|
|
task.task_name for task in stack_trace[0].awaited_by
|
|
]
|
|
for task_name in ["Task-1", "Task-2"]:
|
|
self.assertIn(task_name, tasks_names)
|
|
|
|
# Now ensure that the awaited_by_relationships are correct
|
|
id_to_task = {
|
|
task.task_id: task for task in stack_trace[0].awaited_by
|
|
}
|
|
task_name_to_awaited_by = {
|
|
task.task_name: set(
|
|
id_to_task[awaited.task_name].task_name
|
|
for awaited in task.awaited_by
|
|
)
|
|
for task in stack_trace[0].awaited_by
|
|
}
|
|
self.assertEqual(
|
|
task_name_to_awaited_by,
|
|
{
|
|
"Task-1": set(),
|
|
"Task-2": {"Task-1"},
|
|
},
|
|
)
|
|
|
|
# Now ensure that the coroutine stacks are correct
|
|
coroutine_stacks = {
|
|
task.task_name: sorted(
|
|
tuple(tuple(frame) for frame in coro.call_stack)
|
|
for coro in task.coroutine_stack
|
|
)
|
|
for task in stack_trace[0].awaited_by
|
|
}
|
|
self.assertEqual(
|
|
coroutine_stacks,
|
|
{
|
|
"Task-1": [
|
|
(
|
|
tuple([staggered.__file__, ANY, "staggered_race"]),
|
|
tuple([script_name, 21, "main"]),
|
|
)
|
|
],
|
|
"Task-2": [
|
|
(
|
|
tuple([script_name, 11, "deep"]),
|
|
tuple([script_name, 15, "c1"]),
|
|
tuple(
|
|
[
|
|
staggered.__file__,
|
|
ANY,
|
|
"staggered_race.<locals>.run_one_coro",
|
|
]
|
|
),
|
|
)
|
|
],
|
|
},
|
|
)
|
|
|
|
# Now ensure the coroutine stacks for the awaited_by relationships are correct.
|
|
awaited_by_coroutine_stacks = {
|
|
task.task_name: sorted(
|
|
(
|
|
id_to_task[coro.task_name].task_name,
|
|
tuple(tuple(frame) for frame in coro.call_stack),
|
|
)
|
|
for coro in task.awaited_by
|
|
)
|
|
for task in stack_trace[0].awaited_by
|
|
}
|
|
self.assertEqual(
|
|
awaited_by_coroutine_stacks,
|
|
{
|
|
"Task-1": [],
|
|
"Task-2": [
|
|
(
|
|
"Task-1",
|
|
(
|
|
tuple(
|
|
[staggered.__file__, ANY, "staggered_race"]
|
|
),
|
|
tuple([script_name, 21, "main"]),
|
|
),
|
|
)
|
|
],
|
|
},
|
|
)
|
|
|
|
@skip_if_not_supported
|
|
@unittest.skipIf(
|
|
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
|
"Test only runs on Linux with process_vm_readv support",
|
|
)
|
|
def test_async_global_awaited_by(self):
|
|
port = find_unused_port()
|
|
script = textwrap.dedent(
|
|
f"""\
|
|
import asyncio
|
|
import os
|
|
import random
|
|
import sys
|
|
import socket
|
|
from string import ascii_lowercase, digits
|
|
from test.support import socket_helper, SHORT_TIMEOUT
|
|
|
|
HOST = '127.0.0.1'
|
|
PORT = socket_helper.find_unused_port()
|
|
connections = 0
|
|
|
|
# Connect to the test process
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
class EchoServerProtocol(asyncio.Protocol):
|
|
def connection_made(self, transport):
|
|
global connections
|
|
connections += 1
|
|
self.transport = transport
|
|
|
|
def data_received(self, data):
|
|
self.transport.write(data)
|
|
self.transport.close()
|
|
|
|
async def echo_client(message):
|
|
reader, writer = await asyncio.open_connection(HOST, PORT)
|
|
writer.write(message.encode())
|
|
await writer.drain()
|
|
|
|
data = await reader.read(100)
|
|
assert message == data.decode()
|
|
writer.close()
|
|
await writer.wait_closed()
|
|
# Signal we are ready to sleep
|
|
sock.sendall(b"ready")
|
|
await asyncio.sleep(SHORT_TIMEOUT)
|
|
|
|
async def echo_client_spam(server):
|
|
async with asyncio.TaskGroup() as tg:
|
|
while connections < 1000:
|
|
msg = list(ascii_lowercase + digits)
|
|
random.shuffle(msg)
|
|
tg.create_task(echo_client("".join(msg)))
|
|
await asyncio.sleep(0)
|
|
# at least a 1000 tasks created. Each task will signal
|
|
# when is ready to avoid the race caused by the fact that
|
|
# tasks are waited on tg.__exit__ and we cannot signal when
|
|
# that happens otherwise
|
|
# at this point all client tasks completed without assertion errors
|
|
# let's wrap up the test
|
|
server.close()
|
|
await server.wait_closed()
|
|
|
|
async def main():
|
|
loop = asyncio.get_running_loop()
|
|
server = await loop.create_server(EchoServerProtocol, HOST, PORT)
|
|
async with server:
|
|
async with asyncio.TaskGroup() as tg:
|
|
tg.create_task(server.serve_forever(), name="server task")
|
|
tg.create_task(echo_client_spam(server), name="echo client spam")
|
|
|
|
asyncio.run(main())
|
|
"""
|
|
)
|
|
stack_trace = None
|
|
with os_helper.temp_dir() as work_dir:
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
# Create a socket server to communicate with the target process
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(1)
|
|
script_name = _make_test_script(script_dir, "script", script)
|
|
client_socket = None
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
client_socket, _ = server_socket.accept()
|
|
server_socket.close()
|
|
for _ in range(1000):
|
|
expected_response = b"ready"
|
|
response = client_socket.recv(len(expected_response))
|
|
self.assertEqual(response, expected_response)
|
|
for _ in busy_retry(SHORT_TIMEOUT):
|
|
try:
|
|
all_awaited_by = get_all_awaited_by(p.pid)
|
|
except RuntimeError as re:
|
|
# This call reads a linked list in another process with
|
|
# no synchronization. That occasionally leads to invalid
|
|
# reads. Here we avoid making the test flaky.
|
|
msg = str(re)
|
|
if msg.startswith("Task list appears corrupted"):
|
|
continue
|
|
elif msg.startswith(
|
|
"Invalid linked list structure reading remote memory"
|
|
):
|
|
continue
|
|
elif msg.startswith("Unknown error reading memory"):
|
|
continue
|
|
elif msg.startswith("Unhandled frame owner"):
|
|
continue
|
|
raise # Unrecognized exception, safest not to ignore it
|
|
else:
|
|
break
|
|
# expected: a list of two elements: 1 thread, 1 interp
|
|
self.assertEqual(len(all_awaited_by), 2)
|
|
# expected: a tuple with the thread ID and the awaited_by list
|
|
self.assertEqual(len(all_awaited_by[0]), 2)
|
|
# expected: no tasks in the fallback per-interp task list
|
|
self.assertEqual(all_awaited_by[1], (0, []))
|
|
entries = all_awaited_by[0][1]
|
|
# expected: at least 1000 pending tasks
|
|
self.assertGreaterEqual(len(entries), 1000)
|
|
# the first three tasks stem from the code structure
|
|
main_stack = [
|
|
FrameInfo([taskgroups.__file__, ANY, "TaskGroup._aexit"]),
|
|
FrameInfo(
|
|
[taskgroups.__file__, ANY, "TaskGroup.__aexit__"]
|
|
),
|
|
FrameInfo([script_name, 60, "main"]),
|
|
]
|
|
self.assertIn(
|
|
TaskInfo(
|
|
[ANY, "Task-1", [CoroInfo([main_stack, ANY])], []]
|
|
),
|
|
entries,
|
|
)
|
|
self.assertIn(
|
|
TaskInfo(
|
|
[
|
|
ANY,
|
|
"server task",
|
|
[
|
|
CoroInfo(
|
|
[
|
|
[
|
|
FrameInfo(
|
|
[
|
|
base_events.__file__,
|
|
ANY,
|
|
"Server.serve_forever",
|
|
]
|
|
)
|
|
],
|
|
ANY,
|
|
]
|
|
)
|
|
],
|
|
[
|
|
CoroInfo(
|
|
[
|
|
[
|
|
FrameInfo(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup._aexit",
|
|
]
|
|
),
|
|
FrameInfo(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup.__aexit__",
|
|
]
|
|
),
|
|
FrameInfo(
|
|
[script_name, ANY, "main"]
|
|
),
|
|
],
|
|
ANY,
|
|
]
|
|
)
|
|
],
|
|
]
|
|
),
|
|
entries,
|
|
)
|
|
self.assertIn(
|
|
TaskInfo(
|
|
[
|
|
ANY,
|
|
"Task-4",
|
|
[
|
|
CoroInfo(
|
|
[
|
|
[
|
|
FrameInfo(
|
|
[tasks.__file__, ANY, "sleep"]
|
|
),
|
|
FrameInfo(
|
|
[
|
|
script_name,
|
|
38,
|
|
"echo_client",
|
|
]
|
|
),
|
|
],
|
|
ANY,
|
|
]
|
|
)
|
|
],
|
|
[
|
|
CoroInfo(
|
|
[
|
|
[
|
|
FrameInfo(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup._aexit",
|
|
]
|
|
),
|
|
FrameInfo(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup.__aexit__",
|
|
]
|
|
),
|
|
FrameInfo(
|
|
[
|
|
script_name,
|
|
41,
|
|
"echo_client_spam",
|
|
]
|
|
),
|
|
],
|
|
ANY,
|
|
]
|
|
)
|
|
],
|
|
]
|
|
),
|
|
entries,
|
|
)
|
|
|
|
expected_awaited_by = [
|
|
CoroInfo(
|
|
[
|
|
[
|
|
FrameInfo(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup._aexit",
|
|
]
|
|
),
|
|
FrameInfo(
|
|
[
|
|
taskgroups.__file__,
|
|
ANY,
|
|
"TaskGroup.__aexit__",
|
|
]
|
|
),
|
|
FrameInfo(
|
|
[script_name, 41, "echo_client_spam"]
|
|
),
|
|
],
|
|
ANY,
|
|
]
|
|
)
|
|
]
|
|
tasks_with_awaited = [
|
|
task
|
|
for task in entries
|
|
if task.awaited_by == expected_awaited_by
|
|
]
|
|
self.assertGreaterEqual(len(tasks_with_awaited), 1000)
|
|
|
|
# the final task will have some random number, but it should for
|
|
# sure be one of the echo client spam horde (In windows this is not true
|
|
# for some reason)
|
|
if sys.platform != "win32":
|
|
self.assertEqual(
|
|
tasks_with_awaited[-1].awaited_by,
|
|
entries[-1].awaited_by,
|
|
)
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
finally:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.kill()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
@skip_if_not_supported
|
|
@unittest.skipIf(
|
|
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
|
"Test only runs on Linux with process_vm_readv support",
|
|
)
|
|
def test_self_trace(self):
|
|
stack_trace = get_stack_trace(os.getpid())
|
|
# Is possible that there are more threads, so we check that the
|
|
# expected stack traces are in the result (looking at you Windows!)
|
|
this_tread_stack = None
|
|
# New format: [InterpreterInfo(interpreter_id, [ThreadInfo(...)])]
|
|
for interpreter_info in stack_trace:
|
|
for thread_info in interpreter_info.threads:
|
|
if thread_info.thread_id == threading.get_native_id():
|
|
this_tread_stack = thread_info.frame_info
|
|
break
|
|
if this_tread_stack:
|
|
break
|
|
self.assertIsNotNone(this_tread_stack)
|
|
self.assertEqual(
|
|
this_tread_stack[:2],
|
|
[
|
|
FrameInfo(
|
|
[
|
|
__file__,
|
|
get_stack_trace.__code__.co_firstlineno + 2,
|
|
"get_stack_trace",
|
|
]
|
|
),
|
|
FrameInfo(
|
|
[
|
|
__file__,
|
|
self.test_self_trace.__code__.co_firstlineno + 6,
|
|
"TestGetStackTrace.test_self_trace",
|
|
]
|
|
),
|
|
],
|
|
)
|
|
|
|
@skip_if_not_supported
|
|
@unittest.skipIf(
|
|
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
|
"Test only runs on Linux with process_vm_readv support",
|
|
)
|
|
@requires_subinterpreters
|
|
def test_subinterpreter_stack_trace(self):
|
|
# Test that subinterpreters are correctly handled
|
|
port = find_unused_port()
|
|
|
|
# Calculate subinterpreter code separately and pickle it to avoid f-string issues
|
|
import pickle
|
|
subinterp_code = textwrap.dedent(f'''
|
|
import socket
|
|
import time
|
|
|
|
def sub_worker():
|
|
def nested_func():
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
sock.sendall(b"ready:sub\\n")
|
|
time.sleep(10_000)
|
|
nested_func()
|
|
|
|
sub_worker()
|
|
''').strip()
|
|
|
|
# Pickle the subinterpreter code
|
|
pickled_code = pickle.dumps(subinterp_code)
|
|
|
|
script = textwrap.dedent(
|
|
f"""
|
|
from concurrent import interpreters
|
|
import time
|
|
import sys
|
|
import socket
|
|
import threading
|
|
|
|
# Connect to the test process
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
def main_worker():
|
|
# Function running in main interpreter
|
|
sock.sendall(b"ready:main\\n")
|
|
time.sleep(10_000)
|
|
|
|
def run_subinterp():
|
|
# Create and run subinterpreter
|
|
subinterp = interpreters.create()
|
|
|
|
import pickle
|
|
pickled_code = {pickled_code!r}
|
|
subinterp_code = pickle.loads(pickled_code)
|
|
subinterp.exec(subinterp_code)
|
|
|
|
# Start subinterpreter in thread
|
|
sub_thread = threading.Thread(target=run_subinterp)
|
|
sub_thread.start()
|
|
|
|
# Start main thread work
|
|
main_thread = threading.Thread(target=main_worker)
|
|
main_thread.start()
|
|
|
|
# Keep main thread alive
|
|
main_thread.join()
|
|
sub_thread.join()
|
|
"""
|
|
)
|
|
|
|
with os_helper.temp_dir() as work_dir:
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
|
|
# Create a socket server to communicate with the target process
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(1)
|
|
|
|
script_name = _make_test_script(script_dir, "script", script)
|
|
client_sockets = []
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
|
|
# Accept connections from both main and subinterpreter
|
|
responses = set()
|
|
while len(responses) < 2: # Wait for both "ready:main" and "ready:sub"
|
|
try:
|
|
client_socket, _ = server_socket.accept()
|
|
client_sockets.append(client_socket)
|
|
|
|
# Read the response from this connection
|
|
response = client_socket.recv(1024)
|
|
if b"ready:main" in response:
|
|
responses.add("main")
|
|
if b"ready:sub" in response:
|
|
responses.add("sub")
|
|
except socket.timeout:
|
|
break
|
|
|
|
server_socket.close()
|
|
stack_trace = get_stack_trace(p.pid)
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
finally:
|
|
for client_socket in client_sockets:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.kill()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
# Verify we have multiple interpreters
|
|
self.assertGreaterEqual(len(stack_trace), 1, "Should have at least one interpreter")
|
|
|
|
# Look for main interpreter (ID 0) and subinterpreter (ID > 0)
|
|
main_interp = None
|
|
sub_interp = None
|
|
|
|
for interpreter_info in stack_trace:
|
|
if interpreter_info.interpreter_id == 0:
|
|
main_interp = interpreter_info
|
|
elif interpreter_info.interpreter_id > 0:
|
|
sub_interp = interpreter_info
|
|
|
|
self.assertIsNotNone(main_interp, "Main interpreter should be present")
|
|
|
|
# Check main interpreter has expected stack trace
|
|
main_found = False
|
|
for thread_info in main_interp.threads:
|
|
for frame in thread_info.frame_info:
|
|
if frame.funcname == "main_worker":
|
|
main_found = True
|
|
break
|
|
if main_found:
|
|
break
|
|
self.assertTrue(main_found, "Main interpreter should have main_worker in stack")
|
|
|
|
# If subinterpreter is present, check its stack trace
|
|
if sub_interp:
|
|
sub_found = False
|
|
for thread_info in sub_interp.threads:
|
|
for frame in thread_info.frame_info:
|
|
if frame.funcname in ("sub_worker", "nested_func"):
|
|
sub_found = True
|
|
break
|
|
if sub_found:
|
|
break
|
|
self.assertTrue(sub_found, "Subinterpreter should have sub_worker or nested_func in stack")
|
|
|
|
@skip_if_not_supported
|
|
@unittest.skipIf(
|
|
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
|
"Test only runs on Linux with process_vm_readv support",
|
|
)
|
|
@requires_subinterpreters
|
|
def test_multiple_subinterpreters_with_threads(self):
|
|
# Test multiple subinterpreters, each with multiple threads
|
|
port = find_unused_port()
|
|
|
|
# Calculate subinterpreter codes separately and pickle them
|
|
import pickle
|
|
|
|
# Code for first subinterpreter with 2 threads
|
|
subinterp1_code = textwrap.dedent(f'''
|
|
import socket
|
|
import time
|
|
import threading
|
|
|
|
def worker1():
|
|
def nested_func():
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
sock.sendall(b"ready:sub1-t1\\n")
|
|
time.sleep(10_000)
|
|
nested_func()
|
|
|
|
def worker2():
|
|
def nested_func():
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
sock.sendall(b"ready:sub1-t2\\n")
|
|
time.sleep(10_000)
|
|
nested_func()
|
|
|
|
t1 = threading.Thread(target=worker1)
|
|
t2 = threading.Thread(target=worker2)
|
|
t1.start()
|
|
t2.start()
|
|
t1.join()
|
|
t2.join()
|
|
''').strip()
|
|
|
|
# Code for second subinterpreter with 2 threads
|
|
subinterp2_code = textwrap.dedent(f'''
|
|
import socket
|
|
import time
|
|
import threading
|
|
|
|
def worker1():
|
|
def nested_func():
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
sock.sendall(b"ready:sub2-t1\\n")
|
|
time.sleep(10_000)
|
|
nested_func()
|
|
|
|
def worker2():
|
|
def nested_func():
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
sock.sendall(b"ready:sub2-t2\\n")
|
|
time.sleep(10_000)
|
|
nested_func()
|
|
|
|
t1 = threading.Thread(target=worker1)
|
|
t2 = threading.Thread(target=worker2)
|
|
t1.start()
|
|
t2.start()
|
|
t1.join()
|
|
t2.join()
|
|
''').strip()
|
|
|
|
# Pickle the subinterpreter codes
|
|
pickled_code1 = pickle.dumps(subinterp1_code)
|
|
pickled_code2 = pickle.dumps(subinterp2_code)
|
|
|
|
script = textwrap.dedent(
|
|
f"""
|
|
from concurrent import interpreters
|
|
import time
|
|
import sys
|
|
import socket
|
|
import threading
|
|
|
|
# Connect to the test process
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
def main_worker():
|
|
# Function running in main interpreter
|
|
sock.sendall(b"ready:main\\n")
|
|
time.sleep(10_000)
|
|
|
|
def run_subinterp1():
|
|
# Create and run first subinterpreter
|
|
subinterp = interpreters.create()
|
|
|
|
import pickle
|
|
pickled_code = {pickled_code1!r}
|
|
subinterp_code = pickle.loads(pickled_code)
|
|
subinterp.exec(subinterp_code)
|
|
|
|
def run_subinterp2():
|
|
# Create and run second subinterpreter
|
|
subinterp = interpreters.create()
|
|
|
|
import pickle
|
|
pickled_code = {pickled_code2!r}
|
|
subinterp_code = pickle.loads(pickled_code)
|
|
subinterp.exec(subinterp_code)
|
|
|
|
# Start subinterpreters in threads
|
|
sub1_thread = threading.Thread(target=run_subinterp1)
|
|
sub2_thread = threading.Thread(target=run_subinterp2)
|
|
sub1_thread.start()
|
|
sub2_thread.start()
|
|
|
|
# Start main thread work
|
|
main_thread = threading.Thread(target=main_worker)
|
|
main_thread.start()
|
|
|
|
# Keep main thread alive
|
|
main_thread.join()
|
|
sub1_thread.join()
|
|
sub2_thread.join()
|
|
"""
|
|
)
|
|
|
|
with os_helper.temp_dir() as work_dir:
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
|
|
# Create a socket server to communicate with the target process
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(5) # Allow multiple connections
|
|
|
|
script_name = _make_test_script(script_dir, "script", script)
|
|
client_sockets = []
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
|
|
# Accept connections from main and all subinterpreter threads
|
|
expected_responses = {"ready:main", "ready:sub1-t1", "ready:sub1-t2", "ready:sub2-t1", "ready:sub2-t2"}
|
|
responses = set()
|
|
|
|
while len(responses) < 5: # Wait for all 5 ready signals
|
|
try:
|
|
client_socket, _ = server_socket.accept()
|
|
client_sockets.append(client_socket)
|
|
|
|
# Read the response from this connection
|
|
response = client_socket.recv(1024)
|
|
response_str = response.decode().strip()
|
|
if response_str in expected_responses:
|
|
responses.add(response_str)
|
|
except socket.timeout:
|
|
break
|
|
|
|
server_socket.close()
|
|
stack_trace = get_stack_trace(p.pid)
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
finally:
|
|
for client_socket in client_sockets:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.kill()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
# Verify we have multiple interpreters
|
|
self.assertGreaterEqual(len(stack_trace), 2, "Should have at least two interpreters")
|
|
|
|
# Count interpreters by ID
|
|
interpreter_ids = {interp.interpreter_id for interp in stack_trace}
|
|
self.assertIn(0, interpreter_ids, "Main interpreter should be present")
|
|
self.assertGreaterEqual(len(interpreter_ids), 3, "Should have main + at least 2 subinterpreters")
|
|
|
|
# Count total threads across all interpreters
|
|
total_threads = sum(len(interp.threads) for interp in stack_trace)
|
|
self.assertGreaterEqual(total_threads, 5, "Should have at least 5 threads total")
|
|
|
|
# Look for expected function names in stack traces
|
|
all_funcnames = set()
|
|
for interpreter_info in stack_trace:
|
|
for thread_info in interpreter_info.threads:
|
|
for frame in thread_info.frame_info:
|
|
all_funcnames.add(frame.funcname)
|
|
|
|
# Should find functions from different interpreters and threads
|
|
expected_funcs = {"main_worker", "worker1", "worker2", "nested_func"}
|
|
found_funcs = expected_funcs.intersection(all_funcnames)
|
|
self.assertGreater(len(found_funcs), 0, f"Should find some expected functions, got: {all_funcnames}")
|
|
|
|
@skip_if_not_supported
|
|
@unittest.skipIf(
|
|
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
|
|
"Test only runs on Linux with process_vm_readv support",
|
|
)
|
|
@requires_gil_enabled("Free threaded builds don't have an 'active thread'")
|
|
def test_only_active_thread(self):
|
|
# Test that only_active_thread parameter works correctly
|
|
port = find_unused_port()
|
|
script = textwrap.dedent(
|
|
f"""\
|
|
import time, sys, socket, threading
|
|
|
|
# Connect to the test process
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
def worker_thread(name, barrier, ready_event):
|
|
barrier.wait() # Synchronize thread start
|
|
ready_event.wait() # Wait for main thread signal
|
|
# Sleep to keep thread alive
|
|
time.sleep(10_000)
|
|
|
|
def main_work():
|
|
# Do busy work to hold the GIL
|
|
sock.sendall(b"working\\n")
|
|
count = 0
|
|
while count < 100000000:
|
|
count += 1
|
|
if count % 10000000 == 0:
|
|
pass # Keep main thread busy
|
|
sock.sendall(b"done\\n")
|
|
|
|
# Create synchronization primitives
|
|
num_threads = 3
|
|
barrier = threading.Barrier(num_threads + 1) # +1 for main thread
|
|
ready_event = threading.Event()
|
|
|
|
# Start worker threads
|
|
threads = []
|
|
for i in range(num_threads):
|
|
t = threading.Thread(target=worker_thread, args=(f"Worker-{{i}}", barrier, ready_event))
|
|
t.start()
|
|
threads.append(t)
|
|
|
|
# Wait for all threads to be ready
|
|
barrier.wait()
|
|
|
|
# Signal ready to parent process
|
|
sock.sendall(b"ready\\n")
|
|
|
|
# Signal threads to start waiting
|
|
ready_event.set()
|
|
|
|
# Now do busy work to hold the GIL
|
|
main_work()
|
|
"""
|
|
)
|
|
|
|
with os_helper.temp_dir() as work_dir:
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
|
|
# Create a socket server to communicate with the target process
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(1)
|
|
|
|
script_name = _make_test_script(script_dir, "script", script)
|
|
client_socket = None
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
client_socket, _ = server_socket.accept()
|
|
server_socket.close()
|
|
|
|
# Wait for ready signal
|
|
response = b""
|
|
while b"ready" not in response:
|
|
response += client_socket.recv(1024)
|
|
|
|
# Wait for the main thread to start its busy work
|
|
while b"working" not in response:
|
|
response += client_socket.recv(1024)
|
|
|
|
# Get stack trace with all threads
|
|
unwinder_all = RemoteUnwinder(p.pid, all_threads=True)
|
|
for _ in range(10):
|
|
# Wait for the main thread to start its busy work
|
|
all_traces = unwinder_all.get_stack_trace()
|
|
found = False
|
|
# New format: [InterpreterInfo(interpreter_id, [ThreadInfo(...)])]
|
|
for interpreter_info in all_traces:
|
|
for thread_info in interpreter_info.threads:
|
|
if not thread_info.frame_info:
|
|
continue
|
|
current_frame = thread_info.frame_info[0]
|
|
if (
|
|
current_frame.funcname == "main_work"
|
|
and current_frame.lineno > 15
|
|
):
|
|
found = True
|
|
break
|
|
if found:
|
|
break
|
|
|
|
if found:
|
|
break
|
|
# Give a bit of time to take the next sample
|
|
time.sleep(0.1)
|
|
else:
|
|
self.fail(
|
|
"Main thread did not start its busy work on time"
|
|
)
|
|
|
|
# Get stack trace with only GIL holder
|
|
unwinder_gil = RemoteUnwinder(p.pid, only_active_thread=True)
|
|
gil_traces = unwinder_gil.get_stack_trace()
|
|
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
finally:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.kill()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
# Count total threads across all interpreters in all_traces
|
|
total_threads = sum(len(interpreter_info.threads) for interpreter_info in all_traces)
|
|
self.assertGreater(
|
|
total_threads, 1, "Should have multiple threads"
|
|
)
|
|
|
|
# Count total threads across all interpreters in gil_traces
|
|
total_gil_threads = sum(len(interpreter_info.threads) for interpreter_info in gil_traces)
|
|
self.assertEqual(
|
|
total_gil_threads, 1, "Should have exactly one GIL holder"
|
|
)
|
|
|
|
# Get the GIL holder thread ID
|
|
gil_thread_id = None
|
|
for interpreter_info in gil_traces:
|
|
if interpreter_info.threads:
|
|
gil_thread_id = interpreter_info.threads[0].thread_id
|
|
break
|
|
|
|
# Get all thread IDs from all_traces
|
|
all_thread_ids = []
|
|
for interpreter_info in all_traces:
|
|
for thread_info in interpreter_info.threads:
|
|
all_thread_ids.append(thread_info.thread_id)
|
|
|
|
self.assertIn(
|
|
gil_thread_id,
|
|
all_thread_ids,
|
|
"GIL holder should be among all threads",
|
|
)
|
|
|
|
|
|
class TestUnsupportedPlatformHandling(unittest.TestCase):
|
|
@unittest.skipIf(
|
|
sys.platform in ("linux", "darwin", "win32"),
|
|
"Test only runs on unsupported platforms (not Linux, macOS, or Windows)",
|
|
)
|
|
@unittest.skipIf(sys.platform == "android", "Android raises Linux-specific exception")
|
|
def test_unsupported_platform_error(self):
|
|
with self.assertRaises(RuntimeError) as cm:
|
|
RemoteUnwinder(os.getpid())
|
|
|
|
self.assertIn(
|
|
"Reading the PyRuntime section is not supported on this platform",
|
|
str(cm.exception)
|
|
)
|
|
|
|
class TestDetectionOfThreadStatus(unittest.TestCase):
|
|
@unittest.skipIf(
|
|
sys.platform not in ("linux", "darwin", "win32"),
|
|
"Test only runs on unsupported platforms (not Linux, macOS, or Windows)",
|
|
)
|
|
@unittest.skipIf(sys.platform == "android", "Android raises Linux-specific exception")
|
|
def test_thread_status_detection(self):
|
|
port = find_unused_port()
|
|
script = textwrap.dedent(
|
|
f"""\
|
|
import time, sys, socket, threading
|
|
import os
|
|
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
def sleeper():
|
|
tid = threading.get_native_id()
|
|
sock.sendall(f'ready:sleeper:{{tid}}\\n'.encode())
|
|
time.sleep(10000)
|
|
|
|
def busy():
|
|
tid = threading.get_native_id()
|
|
sock.sendall(f'ready:busy:{{tid}}\\n'.encode())
|
|
x = 0
|
|
while True:
|
|
x = x + 1
|
|
time.sleep(0.5)
|
|
|
|
t1 = threading.Thread(target=sleeper)
|
|
t2 = threading.Thread(target=busy)
|
|
t1.start()
|
|
t2.start()
|
|
sock.sendall(b'ready:main\\n')
|
|
t1.join()
|
|
t2.join()
|
|
sock.close()
|
|
"""
|
|
)
|
|
with os_helper.temp_dir() as work_dir:
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(1)
|
|
|
|
script_name = _make_test_script(script_dir, "thread_status_script", script)
|
|
client_socket = None
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
client_socket, _ = server_socket.accept()
|
|
server_socket.close()
|
|
response = b""
|
|
sleeper_tid = None
|
|
busy_tid = None
|
|
while True:
|
|
chunk = client_socket.recv(1024)
|
|
response += chunk
|
|
if b"ready:main" in response and b"ready:sleeper" in response and b"ready:busy" in response:
|
|
# Parse TIDs from the response
|
|
for line in response.split(b"\n"):
|
|
if line.startswith(b"ready:sleeper:"):
|
|
try:
|
|
sleeper_tid = int(line.split(b":")[-1])
|
|
except Exception:
|
|
pass
|
|
elif line.startswith(b"ready:busy:"):
|
|
try:
|
|
busy_tid = int(line.split(b":")[-1])
|
|
except Exception:
|
|
pass
|
|
break
|
|
|
|
attempts = 10
|
|
statuses = {}
|
|
try:
|
|
unwinder = RemoteUnwinder(p.pid, all_threads=True, mode=PROFILING_MODE_CPU,
|
|
skip_non_matching_threads=False)
|
|
for _ in range(attempts):
|
|
traces = unwinder.get_stack_trace()
|
|
# Find threads and their statuses
|
|
statuses = {}
|
|
for interpreter_info in traces:
|
|
for thread_info in interpreter_info.threads:
|
|
statuses[thread_info.thread_id] = thread_info.status
|
|
|
|
# Check if sleeper thread is idle and busy thread is running
|
|
if (sleeper_tid in statuses and
|
|
busy_tid in statuses and
|
|
statuses[sleeper_tid] == 1 and
|
|
statuses[busy_tid] == 0):
|
|
break
|
|
time.sleep(0.5) # Give a bit of time to let threads settle
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
|
|
self.assertIsNotNone(sleeper_tid, "Sleeper thread id not received")
|
|
self.assertIsNotNone(busy_tid, "Busy thread id not received")
|
|
self.assertIn(sleeper_tid, statuses, "Sleeper tid not found in sampled threads")
|
|
self.assertIn(busy_tid, statuses, "Busy tid not found in sampled threads")
|
|
self.assertEqual(statuses[sleeper_tid], 1, "Sleeper thread should be idle (1)")
|
|
self.assertEqual(statuses[busy_tid], 0, "Busy thread should be running (0)")
|
|
|
|
finally:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
@unittest.skipIf(
|
|
sys.platform not in ("linux", "darwin", "win32"),
|
|
"Test only runs on unsupported platforms (not Linux, macOS, or Windows)",
|
|
)
|
|
@unittest.skipIf(sys.platform == "android", "Android raises Linux-specific exception")
|
|
def test_thread_status_gil_detection(self):
|
|
port = find_unused_port()
|
|
script = textwrap.dedent(
|
|
f"""\
|
|
import time, sys, socket, threading
|
|
import os
|
|
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.connect(('localhost', {port}))
|
|
|
|
def sleeper():
|
|
tid = threading.get_native_id()
|
|
sock.sendall(f'ready:sleeper:{{tid}}\\n'.encode())
|
|
time.sleep(10000)
|
|
|
|
def busy():
|
|
tid = threading.get_native_id()
|
|
sock.sendall(f'ready:busy:{{tid}}\\n'.encode())
|
|
x = 0
|
|
while True:
|
|
x = x + 1
|
|
time.sleep(0.5)
|
|
|
|
t1 = threading.Thread(target=sleeper)
|
|
t2 = threading.Thread(target=busy)
|
|
t1.start()
|
|
t2.start()
|
|
sock.sendall(b'ready:main\\n')
|
|
t1.join()
|
|
t2.join()
|
|
sock.close()
|
|
"""
|
|
)
|
|
with os_helper.temp_dir() as work_dir:
|
|
script_dir = os.path.join(work_dir, "script_pkg")
|
|
os.mkdir(script_dir)
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_socket.bind(("localhost", port))
|
|
server_socket.settimeout(SHORT_TIMEOUT)
|
|
server_socket.listen(1)
|
|
|
|
script_name = _make_test_script(script_dir, "thread_status_script", script)
|
|
client_socket = None
|
|
try:
|
|
p = subprocess.Popen([sys.executable, script_name])
|
|
client_socket, _ = server_socket.accept()
|
|
server_socket.close()
|
|
response = b""
|
|
sleeper_tid = None
|
|
busy_tid = None
|
|
while True:
|
|
chunk = client_socket.recv(1024)
|
|
response += chunk
|
|
if b"ready:main" in response and b"ready:sleeper" in response and b"ready:busy" in response:
|
|
# Parse TIDs from the response
|
|
for line in response.split(b"\n"):
|
|
if line.startswith(b"ready:sleeper:"):
|
|
try:
|
|
sleeper_tid = int(line.split(b":")[-1])
|
|
except Exception:
|
|
pass
|
|
elif line.startswith(b"ready:busy:"):
|
|
try:
|
|
busy_tid = int(line.split(b":")[-1])
|
|
except Exception:
|
|
pass
|
|
break
|
|
|
|
attempts = 10
|
|
statuses = {}
|
|
try:
|
|
unwinder = RemoteUnwinder(p.pid, all_threads=True, mode=PROFILING_MODE_GIL,
|
|
skip_non_matching_threads=False)
|
|
for _ in range(attempts):
|
|
traces = unwinder.get_stack_trace()
|
|
# Find threads and their statuses
|
|
statuses = {}
|
|
for interpreter_info in traces:
|
|
for thread_info in interpreter_info.threads:
|
|
statuses[thread_info.thread_id] = thread_info.status
|
|
|
|
# Check if sleeper thread is idle (status 2 for GIL mode) and busy thread is running
|
|
if (sleeper_tid in statuses and
|
|
busy_tid in statuses and
|
|
statuses[sleeper_tid] == 2 and
|
|
statuses[busy_tid] == 0):
|
|
break
|
|
time.sleep(0.5) # Give a bit of time to let threads settle
|
|
except PermissionError:
|
|
self.skipTest(
|
|
"Insufficient permissions to read the stack trace"
|
|
)
|
|
|
|
self.assertIsNotNone(sleeper_tid, "Sleeper thread id not received")
|
|
self.assertIsNotNone(busy_tid, "Busy thread id not received")
|
|
self.assertIn(sleeper_tid, statuses, "Sleeper tid not found in sampled threads")
|
|
self.assertIn(busy_tid, statuses, "Busy tid not found in sampled threads")
|
|
self.assertEqual(statuses[sleeper_tid], 2, "Sleeper thread should be idle (1)")
|
|
self.assertEqual(statuses[busy_tid], 0, "Busy thread should be running (0)")
|
|
|
|
finally:
|
|
if client_socket is not None:
|
|
client_socket.close()
|
|
p.terminate()
|
|
p.wait(timeout=SHORT_TIMEOUT)
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|