https://github.com/python/cpython/commit/4adffd9efad7052ad0a22743dd678d0201ec0286
commit: 4adffd9efad7052ad0a22743dd678d0201ec0286
branch: main
author: Bartosz SÅ‚awecki <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-04-13T22:10:54Z
summary:

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 <[email protected]>
Co-authored-by: Stan Ulbrych <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-04-13-21-38-50.gh-issue-144881.3kPqXw.rst
M Lib/asyncio/__main__.py
M Lib/asyncio/tools.py

diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py
index 0bf3bdded40200..8ee09b38469d4c 100644
--- a/Lib/asyncio/__main__.py
+++ b/Lib/asyncio/__main__.py
@@ -162,17 +162,29 @@ def interrupt(self) -> None:
         "ps", help="Display a table of all pending tasks in a process"
     )
     ps.add_argument("pid", type=int, help="Process ID to inspect")
+    ps.add_argument(
+        "--retries",
+        type=int,
+        default=3,
+        help="Number of retries on transient attach errors",
+    )
     pstree = subparsers.add_parser(
         "pstree", help="Display a tree of all pending tasks in a process"
     )
     pstree.add_argument("pid", type=int, help="Process ID to inspect")
+    pstree.add_argument(
+        "--retries",
+        type=int,
+        default=3,
+        help="Number of retries on transient attach errors",
+    )
     args = parser.parse_args()
     match args.command:
         case "ps":
-            asyncio.tools.display_awaited_by_tasks_table(args.pid)
+            asyncio.tools.display_awaited_by_tasks_table(args.pid, 
retries=args.retries)
             sys.exit(0)
         case "pstree":
-            asyncio.tools.display_awaited_by_tasks_tree(args.pid)
+            asyncio.tools.display_awaited_by_tasks_tree(args.pid, 
retries=args.retries)
             sys.exit(0)
         case None:
             pass  # continue to the interactive shell
diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py
index 62d6a71557fa37..2ac1738d15c6c7 100644
--- a/Lib/asyncio/tools.py
+++ b/Lib/asyncio/tools.py
@@ -231,27 +231,38 @@ def exit_with_permission_help_text():
     print(
         "Error: The specified process cannot be attached to due to 
insufficient permissions.\n"
         "See the Python documentation for details on required privileges and 
troubleshooting:\n"
-        
"https://docs.python.org/3.14/howto/remote_debugging.html#permission-requirements\n";
+        
"https://docs.python.org/3/howto/remote_debugging.html#permission-requirements\n";,
+        file=sys.stderr,
     )
     sys.exit(1)
 
 
-def _get_awaited_by_tasks(pid: int) -> list:
-    try:
-        return get_all_awaited_by(pid)
-    except RuntimeError as e:
-        while e.__context__ is not None:
-            e = e.__context__
-        print(f"Error retrieving tasks: {e}")
-        sys.exit(1)
-    except PermissionError:
-        exit_with_permission_help_text()
+_TRANSIENT_ERRORS = (RuntimeError, OSError, UnicodeDecodeError, MemoryError)
+
+
+def _get_awaited_by_tasks(pid: int, retries: int = 3) -> list:
+    for attempt in range(retries + 1):
+        try:
+            return get_all_awaited_by(pid)
+        except PermissionError:
+            exit_with_permission_help_text()
+        except ProcessLookupError:
+            print(f"Error: process {pid} not found.", file=sys.stderr)
+            sys.exit(1)
+        except _TRANSIENT_ERRORS as e:
+            if attempt < retries:
+                continue
+            if isinstance(e, RuntimeError):
+                while e.__context__ is not None:
+                    e = e.__context__
+            print(f"Error retrieving tasks: {e}", file=sys.stderr)
+            sys.exit(1)
 
 
-def display_awaited_by_tasks_table(pid: int) -> None:
+def display_awaited_by_tasks_table(pid: int, retries: int = 3) -> None:
     """Build and print a table of all pending tasks under `pid`."""
 
-    tasks = _get_awaited_by_tasks(pid)
+    tasks = _get_awaited_by_tasks(pid, retries=retries)
     table = build_task_table(tasks)
     # Print the table in a simple tabular format
     print(
@@ -262,10 +273,10 @@ def display_awaited_by_tasks_table(pid: int) -> None:
         print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} 
{row[4]:<50} {row[5]:<15} {row[6]:<15}")
 
 
-def display_awaited_by_tasks_tree(pid: int) -> None:
+def display_awaited_by_tasks_tree(pid: int, retries: int = 3) -> None:
     """Build and print a tree of all pending tasks under `pid`."""
 
-    tasks = _get_awaited_by_tasks(pid)
+    tasks = _get_awaited_by_tasks(pid, retries=retries)
     try:
         result = build_async_tree(tasks)
     except CycleFoundException as e:
diff --git 
a/Misc/NEWS.d/next/Library/2026-04-13-21-38-50.gh-issue-144881.3kPqXw.rst 
b/Misc/NEWS.d/next/Library/2026-04-13-21-38-50.gh-issue-144881.3kPqXw.rst
new file mode 100644
index 00000000000000..0812dc9efb6d8b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-04-13-21-38-50.gh-issue-144881.3kPqXw.rst
@@ -0,0 +1,4 @@
+:mod:`asyncio` debugging tools (``python -m asyncio ps`` and ``pstree``)
+now retry automatically on transient errors that can occur when attaching
+to a process under active thread delegation. The number of retries can be
+controlled with the ``--retries`` flag. Patch by Bartosz Sławecki.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to