Skip to content

Prevent thread-pool drain from flush on dead sockets (fixes #710) - #843

Open
mwstowe wants to merge 3 commits into
cherrypy:mainfrom
mwstowe:fix/710-flush-dead-socket
Open

mwstowe wants to merge 3 commits into
cherrypy:mainfrom
mwstowe:fix/710-flush-dead-socket

Conversation

@mwstowe

@mwstowe mwstowe commented Sep 21, 2026

Copy link
Copy Markdown

What kind of change does this PR introduce?

  • 🐞 bug fix

What do these changes do?

BufferedWriter._flush_unlocked in cheroot/makefile.py only guards
io.BlockingIOError. When a connection's underlying socket descriptor
has already been closed/invalidated, flushing the remaining buffered
bytes calls self.raw.write(...) on a dead fd and raises
OSError: [Errno 9] Bad file descriptor (or ValueError: I/O operation on closed file).

This flush is very commonly driven by the finalizer
(IOBase.__del__ → close → flush) while tearing down abruptly
closed TLS connections (client abort, timeout). Letting the exception
escape a finalizer taints the worker thread's teardown path; over time
worker threads die and are not replaced, draining the thread pool until
the server stops accepting/serving requests while otherwise appearing
to run.

This PR catches OSError/ValueError in _flush_unlocked, drops the
unsendable buffered bytes (the connection is gone, there is nowhere to
send them), and returns cleanly.

Regression tests are added in cheroot/test/test_makefile.py
(test_flush_on_dead_socket_is_swallowed,
test_close_on_dead_socket_is_swallowed). They fail without the fix
and pass with it.

Are there changes in behavior for the user?

Only in the failure path: a buffered write/flush to an
already-dead socket no longer propagates an exception. Successful
writes are unaffected. This prevents the gradual thread-pool
exhaustion / server "wedge" that manifests downstream (e.g. SABnzbd's
web UI going blank while the backend keeps working).

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

No. The change is small (a single additional except clause), local to
_flush_unlocked, and covered by focused unit tests using the existing
MockSocket test style.

Related issue number

Fixes #710 (also relevant to the family of reports around
OSError/SSLEOFError in cheroot's SSL socket teardown, e.g.
sabnzbd/sabnzbd#1429).

Checklist

  • I think the code is well written
  • I wrote good commit messages
  • I have added tests to cover my changes
  • I added a changelog fragment under docs/changelog-fragments.d/

BufferedWriter._flush_unlocked could raise OSError (EBADF) or
ValueError when the underlying socket descriptor was already
invalidated. This flush is frequently driven by the finalizer
(IOBase.__del__ -> close -> flush) while tearing down abruptly closed
TLS connections. The escaping exception taints the worker thread
teardown path, gradually draining the thread pool and wedging the
server while it otherwise appears to keep running.

Swallow OSError/ValueError during flush (dropping the unsendable
buffered bytes, since the connection is gone) so a dead connection
cannot take a worker thread with it.

Fixes cherrypy#710

Signed-off-by: Michael Stowe <mstowe@gmail.com>
@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 Sep 21, 2026
@codecov

codecov Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.31%. Comparing base (36d7d30) to head (9310b9b).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #843      +/-   ##
==========================================
+ Coverage   78.16%   78.31%   +0.14%     
==========================================
  Files          41       41              
  Lines        4791     4814      +23     
  Branches      548      549       +1     
==========================================
+ Hits         3745     3770      +25     
+ Misses        906      905       -1     
+ Partials      140      139       -1     

@read-the-docs-community

read-the-docs-community Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

mwstowe and others added 2 commits September 21, 2026 16:26
- Whitelist 'finalizer' and 'teardown' in docs/spelling_wordlist.txt
  (used in the changelog fragment and test docstrings).
- Ignore DAR401 for cheroot/test/test_makefile.py, consistent with the
  existing per-file darglint suppressions for mock socket methods
  (darglint cannot resolve the dynamically raised 'self._exc').

Signed-off-by: Michael Stowe <mstowe@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

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.

SABnzbd client - ENGINE Error in HTTP server: shutting down

1 participant