Fix unclosed socket ResourceWarnings in werkzeug.serving #2517
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR fixes #2421 and addresses the remaining warnings not fixed by PR #2498. It refactors the code from #2321.
What caused the issue
There are three cases which emit a ResourceWarning, involving three different sockets:
werkzeug.serving.BaseWSGIServer.__init__ calls
super().__init__(...)
and assignsself.socket = socket.fromfd(...)
. But self.socket already exists at the time. If you add print(self.socket) before the assignment, you'll see it's not None. It is an unbound socket unconditionally opened by TCPServer.__init__. The unclosed socket warning actually refers to the old socket, not the one returned by socket.fromfd(). Werkzeug's assignment causes the original socket to be destructed/finalized.This explanation answers @davidism's confusion in ResourceWarning for unclosed sockets with development server #2421 (comment). It works fine for http.server because it never reassigns self.socket.
The reloader parent process creates a socket
s
with prepare_socket() and never closes it. ➜ This part was fixed in handle unclosed socket resource warning #2498 by addings.detach()
.The reloader parent process calls
srv = make_server(...)
, which calls socket.fromfd() to duplicate the socket. In fact, srv is almost unused, except for this log_startup() call. The parent calls run_with_reloader(srv.serve_forever, ...), but main_func is only used in the child process, and completely ignored in the parent. Hence, srv.serve_forever is never called, hence, srv.server_close is never called, hence, srv.socket stays open.How this PR fixes it (in probably too much detail)
Case 1 is fixed by just closing the socket first. (If we could make changes to Python itself, it would be slightly more elegant to add a "socket" argument to TCPServer itself, and not open the other one at all. But it's not a big deal.)
Case 3 could be fixed by calling srv.socket.detach() in the reloader parent process. But it didn't feel very elegant. I don't like that the parent calls prepare_socket() and then duplicates the socket unnecessarily. And it would be nicer to close the socket properly instead of detach().
I refactored run_simple to stop using prepare_socket(). The socket is always created with make_server(). The reloader parent process explicitly calls socket_close() at the end. This also reduces code duplication (e.g. the os.unlink call was duplicated).
To help you review this PR, here is a line by line breakdown of prepare_socket() and how it matches make_server():
Demo
I ran
curl -v http://localhost:5000/
andtouch demo.py
at appropriate times.I also tested it with the reloader disabled.
I'd be grateful if someone could try it on Windows, just in case.
Checklist:
CHANGES.rst
summarizing the change and linking to the issue... versionchanged::
entries in any relevant code docs.pre-commit
hooks and fix any issues.pytest
andtox
, no tests failed.By the way: Thank you for your hard work. I'm a big fan.