]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-144881: Add retry logic to asyncio debugging tools (#148530)
authorBartosz Sławecki <bartosz@ilikepython.com>
Mon, 13 Apr 2026 22:10:54 +0000 (00:10 +0200)
committerGitHub <noreply@github.com>
Mon, 13 Apr 2026 22:10:54 +0000 (22:10 +0000)
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>
Lib/asyncio/__main__.py
Lib/asyncio/tools.py
Misc/NEWS.d/next/Library/2026-04-13-21-38-50.gh-issue-144881.3kPqXw.rst [new file with mode: 0644]

index 0bf3bdded40200a9b053de92f711f9260c78d68a..8ee09b38469d4ccea52425d6f40ffb35bcf74379 100644 (file)
@@ -162,17 +162,29 @@ if __name__ == '__main__':
         "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
index 62d6a71557fa37ba60e68c6b0679bbf42cce196f..2ac1738d15c6c729e903373ff205fc30c1dc88d1 100644 (file)
@@ -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 (file)
index 0000000..0812dc9
--- /dev/null
@@ -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.