Break reference cycle created by default in Flask instances.

Flask instances with static folders were creating a reference cycle
via their "static" view function (which held a strong reference back
to the Flask instance to call its `send_static_file` method). This
prevented CPython from freeing the memory for a Flask instance
when all external references to it were released.

Now use a weakref for the back reference to avoid this.

Co-authored-by: Joshua Bronson <jab@users.noreply.github.com>
This commit is contained in:
Bogdan Opanchuk 2020-10-03 07:05:05 -07:00 committed by GitHub
parent 4e8b020494
commit 8efea0ccbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 29 additions and 1 deletions

View file

@ -1,5 +1,6 @@
import os import os
import sys import sys
import weakref
from datetime import timedelta from datetime import timedelta
from itertools import chain from itertools import chain
from threading import Lock from threading import Lock
@ -478,11 +479,14 @@ class Flask(Scaffold):
assert ( assert (
bool(static_host) == host_matching bool(static_host) == host_matching
), "Invalid static_host/host_matching combination" ), "Invalid static_host/host_matching combination"
# Use a weakref to avoid creating a reference cycle between the app
# and the view function (see #3761).
self_ref = weakref.ref(self)
self.add_url_rule( self.add_url_rule(
f"{self.static_url_path}/<path:filename>", f"{self.static_url_path}/<path:filename>",
endpoint="static", endpoint="static",
host=static_host, host=static_host,
view_func=self.send_static_file, view_func=lambda **kw: self_ref().send_static_file(**kw),
) )
# Set the name of the Click group in case someone wants to add # Set the name of the Click group in case someone wants to add

View file

@ -1,8 +1,11 @@
import gc
import re import re
import sys import sys
import time import time
import uuid import uuid
import weakref
from datetime import datetime from datetime import datetime
from platform import python_implementation
from threading import Thread from threading import Thread
import pytest import pytest
@ -16,6 +19,11 @@ from werkzeug.routing import BuildError
import flask import flask
require_cpython_gc = pytest.mark.skipif(
python_implementation() != "CPython", reason="Requires CPython GC behavior",
)
def test_options_work(app, client): def test_options_work(app, client):
@app.route("/", methods=["GET", "POST"]) @app.route("/", methods=["GET", "POST"])
def index(): def index():
@ -1970,3 +1978,19 @@ def test_max_cookie_size(app, client, recwarn):
client.get("/") client.get("/")
assert len(recwarn) == 0 assert len(recwarn) == 0
@require_cpython_gc
def test_app_freed_on_zero_refcount():
# A Flask instance should not create a reference cycle that prevents CPython
# from freeing it when all external references to it are released (see #3761).
gc.disable()
try:
app = flask.Flask(__name__)
assert app.view_functions["static"]
weak = weakref.ref(app)
assert weak() is not None
del app
assert weak() is None
finally:
gc.enable()