Skip to content

Stop statistics from keeping servers and threads alive - #858

Open
rm-you wants to merge 4 commits into
cherrypy:mainfrom
rm-you:fix/statistics-lifetime
Open

rm-you wants to merge 4 commits into
cherrypy:mainfrom
rm-you:fix/statistics-lifetime

Conversation

@rm-you

@rm-you rm-you commented Oct 19, 2026 •

Copy link
Copy Markdown

What kind of change does this PR introduce?

  • 🐞 bug fix
  • 🐣 feature
  • 📋 docs update
  • 📋 tests/coverage improvement
  • 📋 refactoring
  • 💥 other

What do these changes do?

Every HTTPServer registered its statistics in the process-wide logging.statistics when it was constructed, and nothing removed them. Their callbacks hold the server, so every server ever built stayed alive with its gateway: stopped ones, ones whose prepare() failed to bind, and ones that never started. A server is now registered only while it serves, from prepare() to stop(), and clear_stats() replaces its entry while it serves.

Every WorkerThread also left its entry in server.stats['Worker Threads'] when it exited, so a pool that grows and shrinks kept every thread it ever started. On exit, each worker now folds its final numeric values into a fixed set of aggregate totals and removes its record. This releases retired workers even when statistics are enabled, while preserving requests, bytes read and written, work time, and both throughput totals. Throughput retains its existing sum-of-worker-rates definition.

Registration, retirement and total reads share a lock, so moving a worker's counters into the retired totals cannot temporarily omit or double-count them. Statistics reporters still receive ordinary dictionaries. Each worker retires into the collector it joined; clear_stats() creates a fresh collector, so old workers cannot restore discarded counters.

With a reproducer that discards 100 servers, then grows a one-thread pool by 10 and shrinks it back:

cheroot 11.1.2 this change
discarded servers still alive 100 of 100 0
entries in logging.statistics 100 0
worker statistics entries, for 1 live thread 11 1

Regression tests cover server registration and collection, disabled-statistics cleanup, preservation of all six totals, clearing statistics, and repeated worker retirement after real HTTP requests. The enabled-statistics and HTTP-churn regressions both fail against the previous PR head (208ba8dd).

Additional local validation used 400 grow/shrink cycles and 3,200 real HTTP requests, with statistics enabled, disabled, and toggled. After every cycle, only the single live worker and its statistics record remained; after shutdown, no workers remained reachable through weak references following garbage collection. All six totals were checked before and after every retirement batch. In the previous PR revision, the same enabled-statistics workload retained 141 workers after just 20 cycles with only one worker alive.

Under PyPy, the test case that stops a server is marked as an expected failure. There, coverage.py traces with its Python tracer, which keeps every thread that it traced until the run ends, and the stopped server's worker threads still refer to the server. The case passes under PyPy without coverage (--no-cov).

The first commit makes test_high_number_of_file_descriptors disable garbage collection while it runs, as its _garbage_bin fixture meant to: pypytools' nogc() only pauses the collections that its own hooks start, and nothing enables those hooks. Once stopped servers can be collected, along with the test clients that fixtures attach to them, a collection during that test can close those clients' sockets, and the server can then take one of the freed low file descriptors. This failed the test once on CPython 3.14 here. The fixture now uses the gc module. pypytools is no longer imported; I left its pins in dependencies/ alone to keep this change small, and can drop them here or separately.

Are there changes in behavior for the user?

logging.statistics now lists a server only while it serves: not before prepare(), and not after stop(). A server prepared again after a stop is listed again. The stats['Worker Threads'] mapping contains live worker records only. Retired workers' contributions remain included in the server totals without retaining the workers or their callbacks.

Is it a substantial burden for the maintainers to support this?

The change moves registration into the server's lifecycle and adds a small collector for live records and bounded retired totals. Tests cover the lifecycle, counter preservation, resets and real HTTP worker churn.

Related issue number

Fixes #860. A related problem in ThreadPool.shrink(), #861, is fixed in #859.

Checklist

Tip

New to open source contributions? Making a lovely PR is a great read.

  • I wrote descriptive pull request text above
  • The PR relates to only one subject with a clear title
    and description in grammatically correct, complete sentences
  • I think the code is well written
  • Unit tests for the changes exist
  • Integration tests for the changes exist (if applicable)
  • I used the same coding conventions as the rest of the project
  • The new code doesn't generate linter offenses
  • Project documentation (in docs/) and inline docstrings reflect the changes
  • My commits each have a descriptive title and a body explaining the why
    (see good commit messages)
  • I have added a change log entry
    (can be done once the PR number is known; alternatively, you can
    use a related issue number if one exists)
  • I'm planning to squash related commits together before final merge
  • I have read the contribution guide and the
    code of conduct

The original change, tests and description were written by Anthropic Claude Opus 5.5 on behalf of Adam Harwell (@rm-you). The enabled-statistics follow-up, regression tests, real-worker validation and description update were written by Codex GPT-6. Local validation passed the complete suite on Python 3.11.2 (221 passed, 3 existing expected failures) and all configured pre-commit hooks for the changed files, including mypy-stubtest with Python 3.12.

🤖 Generated with Claude Code

@agentscanapp

agentscanapp Bot commented Oct 19, 2026

Copy link
Copy Markdown

Mixed activity

Activity patterns show a mix of organic and automated signals.

View full analysis →

Evidence
  • Very high PR volume (daily): 38 PRs within a single 24-hour window

Last 5 PRs:

This is an automated analysis by AgentScan

@read-the-docs-community

read-the-docs-community Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 cheroot | 🛠️ Build #35374053 | 📁 Comparing 3e19346 against latest (8402e55)

  🔍 Preview build  

4 files changed
± genindex/index.html
± history/index.html
± pkg/cheroot.server/index.html
± pkg/cheroot.test.test_server/index.html

@rm-you
rm-you force-pushed the fix/statistics-lifetime branch from 041c6ec to 2d4083d Compare October 19, 2026 08:12
@pyba-chronographer pyba-chronographer Bot added the bot:chronographer:provided A mark meaning that a new change log entry is present within the patch. label Oct 19, 2026
@codecov

codecov Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.91837% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.34%. Comparing base (8402e55) to head (3e19346).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #858      +/-   ##
==========================================
+ Coverage   78.20%   79.34%   +1.13%     
==========================================
  Files          41       41              
  Lines        4791     4933     +142     
  Branches      548      570      +22     
==========================================
+ Hits         3747     3914     +167     
+ Misses        904      883      -21     
+ Partials      140      136       -4     

test_high_number_of_file_descriptors needs the server to accept its
connection on a file descriptor above the limit, so nothing may free a
lower one while it runs. Its fixture was meant to stop garbage
collection, but pypytools' nogc() only pauses the collections that its
own hooks start, and nothing enables those hooks, so the collector
still ran.

That mattered little while cheroot kept every server alive, along with
the test clients that fixtures attach to servers. Once stopped servers
can be collected, a collection closes those clients' open sockets at
any time, and the server can then take one of the freed low file
descriptors. This failed the test once on CPython 3.14 in CI.

The fixture now disables the collector through the gc module, which
CPython and PyPy both support.

Generated-By: Anthropic Claude Opus 5.5
Signed-off-by: Adam Harwell <flux.adam@gmail.com>
@rm-you
rm-you force-pushed the fix/statistics-lifetime branch from f080648 to e286783 Compare October 19, 2026 10:06
Every HTTPServer registered its statistics in the process-wide
logging.statistics when it was constructed, and nothing removed them.
Their callbacks hold the server, so every server ever built stayed
alive with its gateway: stopped ones, ones whose prepare() failed to
bind, and ones that never started. A server is now registered only
while it serves, from prepare() to stop().

Every worker thread also left its entry in its server's statistics when
it exited, so a pool that grows and shrinks again kept one more entry,
and the retired thread with it, each time. A thread that recorded no
statistics, as when they are disabled, now removes its entry as it
exits. One that recorded some keeps it, so that the totals still count
its work.

Generated-By: Anthropic Claude Opus 5.5
Signed-off-by: Adam Harwell <flux.adam@gmail.com>
@rm-you
rm-you force-pushed the fix/statistics-lifetime branch from e286783 to 208ba8d Compare October 19, 2026 10:23
@rm-you
rm-you marked this pull request as ready for review October 19, 2026 10:23
@rm-you

rm-you commented Oct 19, 2026 •

Copy link
Copy Markdown
Author

As I posted in my other PR, I will be watching these and will try to respond in a timely fashion to any feedback/concerns. I'm not super attached to this code (as it was AI written) but it does seem to do the job and in my first pass review it didn't seem too bad, though I will admit to not fully understanding the cheroot codebase. If anyone has a better idea about how to fix the bug this tackles, I'm happy to defer to that. If anything, I am not a super huge fan of matching on hardcoded strings, but I don't know a good way around that here, and am open to suggestions.

Recorded statistics still retain every retired worker. Fold their final
values into bounded totals and remove each worker record on exit.
Serialize retirement with total reads to count each worker once.

Preserve all six totals, including the sum of per-worker throughput,
and keep old workers from restoring counters after clear_stats(). Add
regressions for enabled statistics, resets and real HTTP worker churn.

Generated-By: Codex GPT-6
Signed-off-by: Adam Harwell <flux.adam@gmail.com>
Describe retention cases and test assertions directly. Explain why a
worker must retire its record from the collector it registered with
when clear_stats() can replace the server's collector.

Assisted-By: OpenAI Codex (GPT-6)
Signed-off-by: Adam Harwell <flux.adam@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentscan:mixed-signals bot:chronographer:provided A mark meaning that a new change log entry is present within the patch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Statistics keep every server, and retired worker threads, alive

1 participant