Edgewall Software

Ticket #190: clear-data-r803.patch

File clear-data-r803.patch, 4.5 kB (added by cboos, 6 months ago)

Workaround the bug by clearing the Context

  • genshi/template/base.py

    # HG changeset patch
    # User Christian Boos <cboos@neuf.fr>
    # Date 1204545530 -3600
    # Node ID 43f0a2eaea926f7db4f9b77e70560bb66e887330
    # Parent  0bf651204a8abeb4f5c875b715e7b7976c50af89
    Workaround a memory leak happening when an exception is raised while rendering a template.
    
    More specifically, when an exception is raised while evaluating Python code located in expressions or code blocks inside a template, there are some situations where the `data` dictionary placed in the `globals()` used for the evaluation will be kept alive. This will have particularly severe consequences if that data dictionary itself references lots of objects, which is not uncommon.
    
    The workaround consists to clear that leaked `data` (a `genshi.template.base.Context` object) so that at least the objects referenced by the Context won't be leaked (the Context itself will still be leaked, but it's far less critical).
    For clearing the Context, we need to clear all the three of `frames`, `pop` and `push` attributes. Note that the problem happens whether `frames` is a real `deque` or the list based one, and the fix is the same in both cases as well.
    
    Fixes #190.
    
    diff --git a/genshi/template/base.py b/genshi/template/base.py
    a b  
    150150            return self.get(name, default) 
    151151        data.setdefault('defined', defined) 
    152152        data.setdefault('value_of', value_of) 
     153 
     154    def clear(self): 
     155        del self.frames 
     156        del self.pop 
     157        del self.push 
    153158 
    154159    def __repr__(self): 
    155160        return repr(list(self.frames)) 
  • genshi/template/eval.py

    diff --git a/genshi/template/eval.py b/genshi/template/eval.py
    a b  
    141141        __traceback_hide__ = 'before_and_this' 
    142142        _globals = self._globals() 
    143143        _globals['__data__'] = data 
    144         return eval(self.code, _globals, {'__data__': data}) 
     144        try: 
     145            return eval(self.code, _globals, {'__data__': data}) 
     146        except: 
     147            data.clear() 
     148            raise 
    145149 
    146150 
    147151class Suite(Code): 
     
    163167        __traceback_hide__ = 'before_and_this' 
    164168        _globals = self._globals() 
    165169        _globals['__data__'] = data 
    166         exec self.code in _globals, data 
     170        try: 
     171            exec self.code in _globals, data 
     172        except: 
     173            data.clear() 
     174            raise 
    167175 
    168176 
    169177UNDEFINED = object() 
  • genshi/template/tests/markup.py

    diff --git a/genshi/template/tests/markup.py b/genshi/template/tests/markup.py
    a b  
    2222from genshi.core import Markup 
    2323from genshi.input import XML 
    2424from genshi.template.base import BadDirectiveError, TemplateSyntaxError 
     25from genshi.template.eval import UndefinedError 
    2526from genshi.template.loader import TemplateLoader, TemplateNotFound 
    2627from genshi.template.markup import MarkupTemplate 
    2728 
     
    109110            self.assertEqual('test.html', e.filename) 
    110111            if sys.version_info[:2] >= (2, 4): 
    111112                self.assertEqual(3, e.lineno) 
     113 
     114    def test_eval_leak_on_exception(self): 
     115        try: 
     116            from itertools import groupby 
     117        except ImportError: 
     118            return # we need groupby to exhibit the problem 
     119        xml = MarkupTemplate(""" 
     120        <html xmlns:py="http://genshi.edgewall.org/"> 
     121          <body> 
     122            <py:for each="i, j in groupby(test, key=lambda x: len(x))"> 
     123              ${say.hello} (this expression will raise an UndefinedError) 
     124            </py:for> 
     125          </body> 
     126        </html> 
     127        """, lookup='lenient') 
     128        def strlen(x): # Note: when using `strlen` instead of the lambda in the 
     129            len(x)     #       `groupby` in the template above, there's no leak 
     130        test = dict.fromkeys(map(str, range(1000, 100000))) 
     131        # snapshot the current memory usage 
     132        import gc 
     133        i = 2 
     134        while i: 
     135            gc.collect() 
     136            num_objects = len(gc.get_objects()) 
     137            try: 
     138                xml.generate(test=test, groupby=groupby, strlen=strlen).render() 
     139            except UndefinedError: 
     140                pass 
     141            i -= 1 # 2 passes needed for reaching fix-point 
     142        # check the memory usage again 
     143        gc.collect() 
     144        self.assertEqual(num_objects + 1, len(gc.get_objects())) 
     145        # Note: the + 1 accounts for the list created by gc.get_objects above 
    112146 
    113147    def test_markup_noescape(self): 
    114148        """