Fixed #26306 -- Fixed memory leak in cached template loader.

This commit is contained in:
Alex Hill 2016-03-09 11:35:39 +08:00 committed by Tim Graham
parent 460dab0b40
commit ecb59cc657
4 changed files with 79 additions and 10 deletions

View File

@ -68,13 +68,24 @@ class Template(object):
reraise(exc, self.backend)
def copy_exception(exc, backend=None):
"""
Create a new TemplateDoesNotExist. Preserve its declared attributes and
template debug data but discard __traceback__, __context__, and __cause__
to make this object suitable for keeping around (in a cache, for example).
"""
backend = backend or exc.backend
new = exc.__class__(*exc.args, tried=exc.tried, backend=backend, chain=exc.chain)
if hasattr(exc, 'template_debug'):
new.template_debug = exc.template_debug
return new
def reraise(exc, backend):
"""
Reraise TemplateDoesNotExist while maintaining template debug information.
"""
new = exc.__class__(*exc.args, tried=exc.tried, backend=backend)
if hasattr(exc, 'template_debug'):
new.template_debug = exc.template_debug
new = copy_exception(exc, backend)
six.reraise(exc.__class__, new, sys.exc_info()[2])

View File

@ -7,6 +7,7 @@ import hashlib
import warnings
from django.template import Origin, Template, TemplateDoesNotExist
from django.template.backends.django import copy_exception
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_bytes
from django.utils.inspect import func_supports_parameter
@ -27,11 +28,31 @@ class Loader(BaseLoader):
return origin.loader.get_contents(origin)
def get_template(self, template_name, template_dirs=None, skip=None):
"""
Perform the caching that gives this loader its name. Often many of the
templates attempted will be missing, so memory use is of concern here.
To keep it in check, caching behavior is a little complicated when a
template is not found. See ticket #26306 for more details.
With template debugging disabled, cache the TemplateDoesNotExist class
for every missing template and raise a new instance of it after
fetching it from the cache.
With template debugging enabled, a unique TemplateDoesNotExist object
is cached for each missing template to preserve debug data. When
raising an exception, Python sets __traceback__, __context__, and
__cause__ attributes on it. Those attributes can contain references to
all sorts of objects up the call chain and caching them creates a
memory leak. Thus, unraised copies of the exceptions are cached and
copies of those copies are raised after they're fetched from the cache.
"""
key = self.cache_key(template_name, template_dirs, skip)
cached = self.get_template_cache.get(key)
if cached:
if isinstance(cached, TemplateDoesNotExist):
raise cached
if isinstance(cached, type) and issubclass(cached, TemplateDoesNotExist):
raise cached(template_name)
elif isinstance(cached, TemplateDoesNotExist):
raise copy_exception(cached)
return cached
try:
@ -39,7 +60,7 @@ class Loader(BaseLoader):
template_name, template_dirs, skip,
)
except TemplateDoesNotExist as e:
self.get_template_cache[key] = e
self.get_template_cache[key] = copy_exception(e) if self.engine.debug else TemplateDoesNotExist
raise
else:
self.get_template_cache[key] = template

View File

@ -25,3 +25,5 @@ Bugfixes
their password to something with such whitespace after a site updated to
Django 1.9 to reset their password. It provides backwards-compatibility for
earlier versions of Django.
* Fixed a memory leak in the cached template loader (:ticket:`26306`).

View File

@ -49,11 +49,46 @@ class CachedLoaderTests(SimpleTestCase):
self.assertEqual(template.origin.template_name, 'index.html')
self.assertEqual(template.origin.loader, self.engine.template_loaders[0].loaders[0])
def test_get_template_missing(self):
def test_get_template_missing_debug_off(self):
"""
With template debugging disabled, the raw TemplateDoesNotExist class
should be cached when a template is missing. See ticket #26306 and
docstrings in the cached loader for details.
"""
self.engine.debug = False
with self.assertRaises(TemplateDoesNotExist):
self.engine.get_template('doesnotexist.html')
e = self.engine.template_loaders[0].get_template_cache['doesnotexist.html']
self.assertEqual(e.args[0], 'doesnotexist.html')
self.engine.get_template('prod-template-missing.html')
e = self.engine.template_loaders[0].get_template_cache['prod-template-missing.html']
self.assertEqual(e, TemplateDoesNotExist)
def test_get_template_missing_debug_on(self):
"""
With template debugging enabled, a TemplateDoesNotExist instance
should be cached when a template is missing.
"""
self.engine.debug = True
with self.assertRaises(TemplateDoesNotExist):
self.engine.get_template('debug-template-missing.html')
e = self.engine.template_loaders[0].get_template_cache['debug-template-missing.html']
self.assertIsInstance(e, TemplateDoesNotExist)
self.assertEqual(e.args[0], 'debug-template-missing.html')
@unittest.skipIf(six.PY2, "Python 2 doesn't set extra exception attributes")
def test_cached_exception_no_traceback(self):
"""
When a TemplateDoesNotExist instance is cached, the cached instance
should not contain the __traceback__, __context__, or __cause__
attributes that Python sets when raising exceptions.
"""
self.engine.debug = True
with self.assertRaises(TemplateDoesNotExist):
self.engine.get_template('no-traceback-in-cache.html')
e = self.engine.template_loaders[0].get_template_cache['no-traceback-in-cache.html']
error_msg = "Cached TemplateDoesNotExist must not have been thrown."
self.assertIsNone(e.__traceback__, error_msg)
self.assertIsNone(e.__context__, error_msg)
self.assertIsNone(e.__cause__, error_msg)
@ignore_warnings(category=RemovedInDjango20Warning)
def test_load_template(self):