Skip to content

Commit 776e790

Browse files
committed
Address review feedback from @vstinner
- Drop _Py_TRACEBACK_MAX_NTHREADS macro; use 0 as sentinel for default 100 inside _Py_DumpTracebackThreads so internal callers don't have to pass the default explicitly. - Rename max_nthreads -> max_threads everywhere for naming consistency with the public Python kwarg. - Add max_threads kwarg to faulthandler.enable(); store in fatal_error.max_threads and pass through faulthandler_dump_traceback to the fatal-signal dump path on both POSIX and Windows. - Drop the three redundant explanatory comments vstinner flagged. - Doc: tighten the limitations bullet, drop implementation-detail mentions of the "..." truncation marker, switch versionchanged directives to "next", document the new enable() kwarg. - Tests: assertEqual exact count, check whole-line "\n...\n" marker, use script_helper.assert_python_ok, drop the default-value test, add test_enable_max_threads exercising the fatal-signal path. - NEWS: trim to two lines, mention all three functions.
1 parent dbceabf commit 776e790

9 files changed

Lines changed: 107 additions & 90 deletions

File tree

Doc/library/faulthandler.rst

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ tracebacks:
3131
* Each string is limited to 500 characters.
3232
* Only the filename, the function name and the line number are
3333
displayed. (no source code)
34-
* It is limited to 100 frames per thread, and by default to 100 threads
35-
total in newest-first order (configurable via *max_threads*).
34+
* It is limited to 100 frames per thread, and 100 threads
35+
(configurable via *max_threads*).
3636
* The order is reversed: the most recent call is shown first.
3737

3838
By default, the Python traceback is written to :data:`sys.stderr`. To see
@@ -60,14 +60,14 @@ Dumping the traceback
6060

6161
Dump the tracebacks of all threads into *file*. If *all_threads* is
6262
``False``, dump only the current thread. *max_threads* caps the number
63-
of threads dumped; a ``...`` marker is written if there are more.
63+
of threads dumped.
6464

6565
.. seealso:: :func:`traceback.print_tb`, which can be used to print a traceback object.
6666

6767
.. versionchanged:: 3.5
6868
Added support for passing file descriptor to this function.
6969

70-
.. versionchanged:: 3.15
70+
.. versionchanged:: next
7171
Added the *max_threads* keyword argument.
7272

7373

@@ -105,7 +105,7 @@ instead of the stack, even if the operating system supports dumping stacks.
105105
Fault handler state
106106
-------------------
107107

108-
.. function:: enable(file=sys.stderr, all_threads=True, c_stack=True)
108+
.. function:: enable(file=sys.stderr, all_threads=True, c_stack=True, *, max_threads=100)
109109

110110
Enable the fault handler: install handlers for the :const:`~signal.SIGSEGV`,
111111
:const:`~signal.SIGFPE`, :const:`~signal.SIGABRT`, :const:`~signal.SIGBUS`
@@ -121,6 +121,8 @@ Fault handler state
121121
traceback, unless the system does not support it. See :func:`dump_c_stack` for
122122
more information on compatibility.
123123

124+
*max_threads* caps the number of threads dumped when a fatal signal fires.
125+
124126
.. versionchanged:: 3.5
125127
Added support for passing file descriptor to this function.
126128

@@ -138,6 +140,9 @@ Fault handler state
138140
.. versionchanged:: 3.14
139141
The dump now displays the C stack trace if *c_stack* is true.
140142

143+
.. versionchanged:: next
144+
Added the *max_threads* keyword argument.
145+
141146
.. function:: disable()
142147

143148
Disable the fault handler: uninstall the signal handlers installed by
@@ -159,8 +164,7 @@ Dumping the tracebacks after a timeout
159164
:c:func:`!_exit` exits the process immediately, which means it doesn't do any
160165
cleanup like flushing file buffers.) If the function is called twice, the new
161166
call replaces previous parameters and resets the timeout. The timer has a
162-
sub-second resolution. *max_threads* caps the number of threads dumped;
163-
a ``...`` marker is written if there are more.
167+
sub-second resolution. *max_threads* caps the number of threads dumped.
164168

165169
The *file* must be kept open until the traceback is dumped or
166170
:func:`cancel_dump_traceback_later` is called: see :ref:`issue with file
@@ -174,7 +178,7 @@ Dumping the tracebacks after a timeout
174178
.. versionchanged:: 3.7
175179
This function is now always available.
176180

177-
.. versionchanged:: 3.15
181+
.. versionchanged:: next
178182
Added the *max_threads* keyword argument.
179183

180184
.. function:: cancel_dump_traceback_later()

Include/internal/pycore_faulthandler.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ struct _faulthandler_runtime_state {
5757
void *exc_handler;
5858
#endif
5959
int c_stack;
60+
unsigned int max_threads;
6061
} fatal_error;
6162

6263
struct {
@@ -68,8 +69,7 @@ struct _faulthandler_runtime_state {
6869
int exit;
6970
char *header;
7071
size_t header_len;
71-
/* Thread-count cap passed to _Py_DumpTracebackThreads. */
72-
unsigned int max_nthreads;
72+
unsigned int max_threads;
7373
/* The main thread always holds this lock. It is only released when
7474
faulthandler_thread() is interrupted before this thread exits, or at
7575
Python exit. */

Include/internal/pycore_traceback.h

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,11 @@ extern void _Py_DumpTraceback(
5858
5959
This function is signal safe. */
6060

61-
/* The historical per-call cap on the number of threads dumped by
62-
_Py_DumpTracebackThreads; surfaced as the public default for the
63-
max_threads kwarg on faulthandler.dump_traceback{,_later}. */
64-
#define _Py_TRACEBACK_MAX_NTHREADS 100
65-
66-
/* max_nthreads is the per-call cap. Pass _Py_TRACEBACK_MAX_NTHREADS
67-
for the historical default; user-facing callers should use the
68-
clinic-supplied default of the public Python API. */
6961
extern const char* _Py_DumpTracebackThreads(
7062
int fd,
7163
PyInterpreterState *interp,
7264
PyThreadState *current_tstate,
73-
unsigned int max_nthreads);
65+
unsigned int max_threads);
7466

7567
/* Write a Unicode object into the file descriptor fd. Encode the string to
7668
ASCII using the backslashreplace error handler.

Lib/test/test_faulthandler.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ def test_dump_traceback_later_twice(self):
722722
def test_dump_traceback_max_threads(self):
723723
# max_threads caps the dump and writes "...\n" when truncated.
724724
# Spawn N worker threads, dump with cap < N, and verify the
725-
# marker is present and at most CAP thread headers are written.
725+
# marker is present and exactly CAP thread headers are written.
726726
code = dedent("""
727727
import faulthandler
728728
import sys
@@ -749,31 +749,42 @@ def worker():
749749
for t in threads:
750750
t.join()
751751
""").strip()
752-
# spawn_python merges stderr into stdout by default.
753-
with support.SuppressCrashReport():
754-
process = script_helper.spawn_python('-c', code)
755-
with process:
756-
output, _ = process.communicate()
757-
process.wait()
758-
# Truncation marker is written when the cap is hit.
759-
self.assertIn(b"...\n", output)
760-
# Cap of 3 means at most 3 thread headers in the dump.
761-
self.assertLessEqual(output.count(b"Thread 0x"), 3)
762-
763-
def test_dump_traceback_max_threads_default(self):
764-
# The default max_threads of 100 preserves historical behavior:
765-
# no truncation when the live thread count is below the cap.
752+
proc = script_helper.assert_python_ok('-c', code)
753+
output = proc.err
754+
# Truncation marker is written on its own line when the cap is hit.
755+
self.assertIn(b"\n...\n", output)
756+
# Cap of 3 means exactly 3 thread headers in the dump.
757+
self.assertEqual(output.count(b"Thread 0x"), 3)
758+
759+
@skip_segfault_on_android
760+
def test_enable_max_threads(self):
761+
# enable(max_threads=N) caps the thread dump produced when a
762+
# fatal signal fires.
766763
code = dedent("""
767764
import faulthandler
768-
import sys
769-
faulthandler.dump_traceback(file=sys.stderr)
765+
import threading
766+
767+
NTHREADS = 6
768+
CAP = 3
769+
770+
ready = threading.Barrier(NTHREADS + 1)
771+
stop = threading.Event()
772+
773+
def worker():
774+
ready.wait()
775+
stop.wait()
776+
777+
for _ in range(NTHREADS):
778+
threading.Thread(target=worker, daemon=True).start()
779+
ready.wait()
780+
faulthandler.enable(max_threads=CAP)
781+
faulthandler._sigsegv()
770782
""").strip()
771-
with support.SuppressCrashReport():
772-
process = script_helper.spawn_python('-c', code)
773-
with process:
774-
output, _ = process.communicate()
775-
process.wait()
776-
self.assertNotIn(b"...\n", output)
783+
output, exitcode = self.get_output(code)
784+
output = '\n'.join(output)
785+
# Cap of 3 means the dump is truncated with "..." on its own line.
786+
self.assertIn("\n...\n", output)
787+
self.assertNotEqual(exitcode, 0)
777788

778789
@unittest.skipIf(not hasattr(faulthandler, "register"),
779790
"need faulthandler.register")
Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,2 @@
1-
Add a *max_threads* keyword argument to :func:`faulthandler.dump_traceback`
2-
and :func:`faulthandler.dump_traceback_later`, raising the per-call cap on
3-
the number of threads dumped (previously a hard-coded ``MAX_NTHREADS = 100``
4-
in :file:`Python/traceback.c`). Useful for server processes with many
5-
worker or gRPC threads, where dump order (newest-thread-first) means the
6-
historical 100-thread cap silently elided the main thread. The default of
7-
``100`` preserves existing behavior.
1+
Add a *max_threads* keyword argument to :func:`faulthandler.dump_traceback`,
2+
:func:`faulthandler.dump_traceback_later`, and :func:`faulthandler.enable`.

Modules/clinic/faulthandler.c.h

Lines changed: 28 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)