Skip to content

Shut down task dispatcher's worker threads in BaseWSGIServer.close() - #497

Closed
agu2347 wants to merge 1 commit into
Pylons:mainfrom
agu2347:fix-close-shutdown-dispatcher
Closed

Shut down task dispatcher's worker threads in BaseWSGIServer.close()#497
agu2347 wants to merge 1 commit into
Pylons:mainfrom
agu2347:fix-close-shutdown-dispatcher

Conversation

@agu2347

@agu2347 agu2347 commented Jul 28, 2026

Copy link
Copy Markdown

Fixes #480.

MultiSocketServer.close() already shuts down its task dispatcher before closing sockets, but BaseWSGIServer.close() did not -- an inconsistency between two classes that both own/share a task_dispatcher. This matters more than it might first appear: for the common single-listen-address case, create_server() returns a bare BaseWSGIServer directly (not wrapped in MultiSocketServer, per its own "we can just return the last server" optimization), so calling .close() on it is the only place a caller has to shut things down without also calling .run() first. Without this fix, doing so left the dispatcher's worker (daemon) threads running.

BaseWSGIServer.run() already calls task_dispatcher.shutdown() in its own SystemExit/KeyboardInterrupt handler, but that's a separate code path that only triggers if .run() was actually called and interrupted -- it doesn't help a caller who creates a server, then closes it directly (e.g. after a socket-binding failure elsewhere, or in test teardown, as described in the issue).

Testing: verified directly: created a real server with several worker threads, confirmed close() previously left those threads running (observable via threading.active_count()), and confirmed the fix correctly stops them. Also verified the multi-listen-address case (MultiSocketServer, which shares one task_dispatcher across multiple wrapped BaseWSGIServer instances) continues to work correctly with no double-shutdown issues, since ThreadedTaskDispatcher.shutdown() is idempotent (a no-op if no threads remain).

Added a direct regression test for BaseWSGIServer.close() specifically (using the same DummyTaskDispatcher mock and was_shutdown flag pattern the existing, already-passing test_run test uses for the separate .run()-triggered shutdown path). I confirmed the new test fails with the original code and passes with the fix. Ran the full existing test_server.py suite (32 passed: 31 baseline + 1 new) and the broader project test suite (752 passed, 50 skipped; the 3 remaining failures are pre-existing socket-binding tests unrelated to this change).

MultiSocketServer.close() already shuts down its task dispatcher
before closing sockets, but BaseWSGIServer.close() did not -- an
inconsistency between two classes that both own/share a
task_dispatcher. This matters more than it might first appear: for
the common single-listen-address case, create_server() returns a
bare BaseWSGIServer directly (not wrapped in MultiSocketServer, per
its own "we can just return the last server" optimization), so
calling .close() on it is the *only* place a caller has to shut
things down without also calling .run() first. Without this fix,
doing so left the dispatcher's worker (daemon) threads running.

BaseWSGIServer.run() already calls task_dispatcher.shutdown() in its
own SystemExit/KeyboardInterrupt handler, but that's a separate code
path that only triggers if .run() was actually called and interrupted
-- it doesn't help a caller who creates a server, then closes it
directly (e.g. after a socket-binding failure elsewhere, or in test
teardown, as described in the issue).

Verified directly: created a real server with several worker threads,
confirmed close() previously left those threads running (observable
via threading.active_count()), and confirmed the fix correctly stops
them. Also verified the multi-listen-address case (MultiSocketServer,
which shares one task_dispatcher across multiple wrapped
BaseWSGIServer instances) continues to work correctly with no
double-shutdown issues, since ThreadedTaskDispatcher.shutdown() is
idempotent (a no-op if no threads remain).

Added a direct regression test for BaseWSGIServer.close() specifically
(using the same DummyTaskDispatcher mock and was_shutdown flag pattern
the existing, already-passing test_run test uses for the separate
.run()-triggered shutdown path). Confirmed the new test fails with the
original code and passes with the fix. Ran the full existing
test_server.py suite (32 passed: 31 baseline + 1 new) and the broader
project test suite (752 passed, 50 skipped; the 3 remaining failures
are pre-existing socket-binding tests unrelated to this change).

Fixes Pylons#480
@kgaughan

Copy link
Copy Markdown
Member

This looks like it's LLM generated, which is a red flag. I need to consult with others first and also check that you're not engaging in reputation laundering.

@digitalresistor

Copy link
Copy Markdown
Member

I've fixed this, alongside a bunch of other shutdown issues in this PR instead: #499

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Waitress leaves background threads running when socket creation fails

3 participants