Skip to content

Commit 4adffd9

Browse files
johnslavikpablogsalStanFromIreland
authored
gh-144881: Add retry logic to asyncio debugging tools (#148530)
Transient errors can occur when attaching to a process that is actively using thread delegation (e.g. asyncio.to_thread). Add a retry loop to _get_awaited_by_tasks for RuntimeError, OSError, UnicodeDecodeError, and MemoryError, and expose --retries CLI flag on both `ps` and `pstree` subcommands (default: 3). Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> Co-authored-by: Stan Ulbrych <stan@python.org>
1 parent bf452f7 commit 4adffd9

File tree

3 files changed

+44
-17
lines changed

3 files changed

+44
-17
lines changed

Lib/asyncio/__main__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,17 +162,29 @@ def interrupt(self) -> None:
162162
"ps", help="Display a table of all pending tasks in a process"
163163
)
164164
ps.add_argument("pid", type=int, help="Process ID to inspect")
165+
ps.add_argument(
166+
"--retries",
167+
type=int,
168+
default=3,
169+
help="Number of retries on transient attach errors",
170+
)
165171
pstree = subparsers.add_parser(
166172
"pstree", help="Display a tree of all pending tasks in a process"
167173
)
168174
pstree.add_argument("pid", type=int, help="Process ID to inspect")
175+
pstree.add_argument(
176+
"--retries",
177+
type=int,
178+
default=3,
179+
help="Number of retries on transient attach errors",
180+
)
169181
args = parser.parse_args()
170182
match args.command:
171183
case "ps":
172-
asyncio.tools.display_awaited_by_tasks_table(args.pid)
184+
asyncio.tools.display_awaited_by_tasks_table(args.pid, retries=args.retries)
173185
sys.exit(0)
174186
case "pstree":
175-
asyncio.tools.display_awaited_by_tasks_tree(args.pid)
187+
asyncio.tools.display_awaited_by_tasks_tree(args.pid, retries=args.retries)
176188
sys.exit(0)
177189
case None:
178190
pass # continue to the interactive shell

Lib/asyncio/tools.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -231,27 +231,38 @@ def exit_with_permission_help_text():
231231
print(
232232
"Error: The specified process cannot be attached to due to insufficient permissions.\n"
233233
"See the Python documentation for details on required privileges and troubleshooting:\n"
234-
"https://docs.python.org/3.14/howto/remote_debugging.html#permission-requirements\n"
234+
"https://docs.python.org/3/howto/remote_debugging.html#permission-requirements\n",
235+
file=sys.stderr,
235236
)
236237
sys.exit(1)
237238

238239

239-
def _get_awaited_by_tasks(pid: int) -> list:
240-
try:
241-
return get_all_awaited_by(pid)
242-
except RuntimeError as e:
243-
while e.__context__ is not None:
244-
e = e.__context__
245-
print(f"Error retrieving tasks: {e}")
246-
sys.exit(1)
247-
except PermissionError:
248-
exit_with_permission_help_text()
240+
_TRANSIENT_ERRORS = (RuntimeError, OSError, UnicodeDecodeError, MemoryError)
241+
242+
243+
def _get_awaited_by_tasks(pid: int, retries: int = 3) -> list:
244+
for attempt in range(retries + 1):
245+
try:
246+
return get_all_awaited_by(pid)
247+
except PermissionError:
248+
exit_with_permission_help_text()
249+
except ProcessLookupError:
250+
print(f"Error: process {pid} not found.", file=sys.stderr)
251+
sys.exit(1)
252+
except _TRANSIENT_ERRORS as e:
253+
if attempt < retries:
254+
continue
255+
if isinstance(e, RuntimeError):
256+
while e.__context__ is not None:
257+
e = e.__context__
258+
print(f"Error retrieving tasks: {e}", file=sys.stderr)
259+
sys.exit(1)
249260

250261

251-
def display_awaited_by_tasks_table(pid: int) -> None:
262+
def display_awaited_by_tasks_table(pid: int, retries: int = 3) -> None:
252263
"""Build and print a table of all pending tasks under `pid`."""
253264

254-
tasks = _get_awaited_by_tasks(pid)
265+
tasks = _get_awaited_by_tasks(pid, retries=retries)
255266
table = build_task_table(tasks)
256267
# Print the table in a simple tabular format
257268
print(
@@ -262,10 +273,10 @@ def display_awaited_by_tasks_table(pid: int) -> None:
262273
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<50} {row[5]:<15} {row[6]:<15}")
263274

264275

265-
def display_awaited_by_tasks_tree(pid: int) -> None:
276+
def display_awaited_by_tasks_tree(pid: int, retries: int = 3) -> None:
266277
"""Build and print a tree of all pending tasks under `pid`."""
267278

268-
tasks = _get_awaited_by_tasks(pid)
279+
tasks = _get_awaited_by_tasks(pid, retries=retries)
269280
try:
270281
result = build_async_tree(tasks)
271282
except CycleFoundException as e:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`asyncio` debugging tools (``python -m asyncio ps`` and ``pstree``)
2+
now retry automatically on transient errors that can occur when attaching
3+
to a process under active thread delegation. The number of retries can be
4+
controlled with the ``--retries`` flag. Patch by Bartosz Sławecki.

0 commit comments

Comments
 (0)