Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Close open resources #3250

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open

Conversation

kurtmckee
Copy link
Contributor

There are several places in the test suite and in the production code where resources are opened but not closed. This results in ResourceWarnings while the test suite is running (which Python normally ignores), and is revealed by setting pytest's filterwarning=errors.

This PR introduces the following changes:

  • Close sockets that are opened by production code but unclosed when an exception is encountered
  • Close resources that are opened by the test suite
  • Configure pytest to escalate warnings to errors

If this is merged, it will help ensure that gunicorn consistently closes resources (files and sockets).

@pajod
Copy link
Contributor

pajod commented Jul 26, 2024

Huh. Making warnings fatal keeps CI all green despite the garbage collected file handle in

config_json = json.load(open(cfg.logconfig_json))
- because we are not testing that in the first place!

Suggested followup: master...pajod:gunicorn:close-resources-followup


No objections, though I suspect also a few DeprecationWarnings currently not in CI will pop up - not necessarily a bad thing.

Linking related issues for easier discovery:

Copy link
Owner

@benoitc benoitc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please answer to incline comment. Also revert the change corresponding to the socket. This is not needed.

try:
self.sock = socket.socket(address_family, socket.SOCK_DGRAM)
self.sock.connect(cfg.statsd_host)
except Exception:
self.sock = None
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the garbage collector takes care about it?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ddon't try to close the cocket there. None should be ok.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the garbage collector takes care about it?

Even GC reliably working in our favor is no excuse. We should never make the WSGI application developer figure out which warning is our fault and harmless, and which one is triggered by their code and should be looked into.

Copy link
Owner

@benoitc benoitc Aug 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

closing sockets like this is unwanted also. The socket is not supposed to be created at this step. So just making it going to None will be enough.

However this is missing a more elaborated error handler to display what's going wrong.

Copy link
Contributor Author

@kurtmckee kurtmckee Aug 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've answered more fully below; the summary is that relying on the garbage collector without calling .close() will cause Python to throw a ResourceWarning. Simply creating the socket instance is sufficient to cause this problem, as demonstrated in the reproducer code I wrote below.

@@ -76,6 +76,9 @@ main = "gunicorn.app.pasterapp:serve"
norecursedirs = ["examples", "lib", "local", "src"]
testpaths = ["tests/"]
addopts = "--assert=plain --cov=gunicorn --cov-report=xml"
filterwarnings = [
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've answered more fully below; the summary is that this ensures the test suite catches ResourceWarnings that might be introduced in the future.

@kurtmckee
Copy link
Contributor Author

Hello Benoit! Thanks for reviewing this PR. I'm responding here to offer a complete understanding of the changes.

shouldn't the garbage collector takes care about it?

It will, but if the socket's .close() method is not called explicitly then Python will raise a ResourceWarning. ResourceWarning is ignored by Python default, but developers are free to modify how Python handles warnings, which is why this PR configures pytest to escalate warnings to errors and then resolves all of the ResourceWarnings that appeared as a result.


don't try to close the socket there. None should be ok.

None is insufficient. Here's a reproducer:

import socket
import warnings

# Configure Python to stop ignoring `ResourceWarning`s.
warnings.filterwarnings("always")


def rely_on_gc():
    # When `s` is reassigned, gc will clean up the socket
    # and a ResourceWarning will be thrown.
    print("rely_on_gc(): start")
    s = socket.socket()
    s = None
    print("rely_on_gc(): end")


def explicit_close():
    print("explicit_close(): start")
    s = socket.socket()
    s.close()
    s = None
    print("explicit_close(): end")


rely_on_gc()
print("---")
explicit_close()

And here's the output:

$ python demo.py 
rely_on_gc(): start
/home/kurt/dev/demo.py:13: ResourceWarning: unclosed <socket.socket fd=3, family=2, type=1, proto=0, laddr=('0.0.0.0', 0)>
  s = None
ResourceWarning: Enable tracemalloc to get the object allocation traceback
rely_on_gc(): end
---
explicit_close(): start
explicit_close(): end

As demonstrated above, setting self.sock to None is insufficient; the socket's .close() method must be called before garbage collection to prevent a ResourceWarning.


why [add filterwarnings to the pytest configuration]?

This was actually the first step I took to reveal where ResourceWarnings were getting thrown in the codebase. However, I committed the change only after all ResourceWarnings were resolved.

Adding this configuration to the test suite ensures that future code changes that introduce ResourceWarnings will be caught before they re-enter the gunicorn codebase.


revert the change corresponding to the socket. This is not needed

That change is the entire point of this PR -- it guarantees that Python does not throw a ResourceWarning in gunicorn's codebase, and allows developers like me to display all warnings in our deployed environments without seeing a ResourceWarning from within gunicorn.

@kurtmckee
Copy link
Contributor Author

Hello @benoitc! Please let me know if you have additional questions or feedback. Thanks!

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.

3 participants