From 77cf7696a678149d05286a2b88255a156c47179a Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 13 Apr 2026 23:07:57 +0200 Subject: [PATCH 1/7] gh-144881: Add retry logic to asyncio debugging tools 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). --- Lib/asyncio/__main__.py | 16 +++++++++++++-- Lib/asyncio/tools.py | 43 +++++++++++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 0bf3bdded40200..cf3cdeac5ef644 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 (default: 3)", + ) 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 (default: 3)", + ) 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..5e1ea6d372a228 100644 --- a/Lib/asyncio/tools.py +++ b/Lib/asyncio/tools.py @@ -236,22 +236,37 @@ def exit_with_permission_help_text(): 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: + print( + f"Transient error while reading process state " + f"(attempt {attempt + 1}/{retries + 1}), retrying...", + file=sys.stderr, + ) + 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 +277,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: From 6f7dde40401be2629ec012457bbb66000fef85c7 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 13 Apr 2026 23:13:27 +0200 Subject: [PATCH 2/7] gh-144881: Remove noisy retry progress messages Silent retries are less confusing; only report on final failure. --- Lib/asyncio/tools.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py index 5e1ea6d372a228..086bb684d6fcad 100644 --- a/Lib/asyncio/tools.py +++ b/Lib/asyncio/tools.py @@ -250,11 +250,6 @@ def _get_awaited_by_tasks(pid: int, retries: int = 3) -> list: sys.exit(1) except _TRANSIENT_ERRORS as e: if attempt < retries: - print( - f"Transient error while reading process state " - f"(attempt {attempt + 1}/{retries + 1}), retrying...", - file=sys.stderr, - ) continue if isinstance(e, RuntimeError): while e.__context__ is not None: From 6b9db6bc96c9b511fb4dfeb085488d82dfe0c192 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 13 Apr 2026 23:16:09 +0200 Subject: [PATCH 3/7] gh-144881: Drop redundant default from --retries help text --- Lib/asyncio/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index cf3cdeac5ef644..8ee09b38469d4c 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -166,7 +166,7 @@ def interrupt(self) -> None: "--retries", type=int, default=3, - help="Number of retries on transient attach errors (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" @@ -176,7 +176,7 @@ def interrupt(self) -> None: "--retries", type=int, default=3, - help="Number of retries on transient attach errors (default: 3)", + help="Number of retries on transient attach errors", ) args = parser.parse_args() match args.command: From 30b8aa5fc12b7fcb7cfa3252aff79cdaa86b1877 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 13 Apr 2026 23:30:08 +0200 Subject: [PATCH 4/7] gh-144881: Send permission error message to stderr --- Lib/asyncio/tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py index 086bb684d6fcad..fd15da4f12cb22 100644 --- a/Lib/asyncio/tools.py +++ b/Lib/asyncio/tools.py @@ -231,7 +231,8 @@ 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.14/howto/remote_debugging.html#permission-requirements\n", + file=sys.stderr, ) sys.exit(1) From 81a8e55e8f28df562f006c1cfc9d8576bec1a55d Mon Sep 17 00:00:00 2001 From: johnslavik Date: Mon, 13 Apr 2026 23:39:11 +0200 Subject: [PATCH 5/7] gh-144881: Add NEWS entry --- .../Library/2026-04-13-21-38-50.gh-issue-144881.3kPqXw.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-13-21-38-50.gh-issue-144881.3kPqXw.rst 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..0753382a2b9211 --- /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. From 6c20c90d7c158067c43090142f161f04438ec753 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Mon, 13 Apr 2026 22:40:05 +0100 Subject: [PATCH 6/7] Update Lib/asyncio/tools.py Co-authored-by: Stan Ulbrych --- Lib/asyncio/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py index fd15da4f12cb22..2ac1738d15c6c7 100644 --- a/Lib/asyncio/tools.py +++ b/Lib/asyncio/tools.py @@ -231,7 +231,7 @@ 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) From 12fcd087a03151ef98b4267cbc20b09ab582a48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Mon, 13 Apr 2026 23:41:55 +0200 Subject: [PATCH 7/7] Add attribution --- .../next/Library/2026-04-13-21-38-50.gh-issue-144881.3kPqXw.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 0753382a2b9211..0812dc9efb6d8b 100644 --- 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 @@ -1,4 +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. +controlled with the ``--retries`` flag. Patch by Bartosz Sławecki.