Opened 15 years ago
Last modified 8 years ago
#373 assigned defect
Weird 'StripDirective' object is not iterable error
Reported by: | cboos | Owned by: | cmlenz |
---|---|---|---|
Priority: | minor | Milestone: | 0.9 |
Component: | Template processing | Version: | devel |
Keywords: | patch | Cc: | rjollos |
Description
Found the following traceback in the trac.log for the demo instance for Trac 0.11:
2010-01-08 09:52:35,605 Trac[main] ERROR: Internal Server Error: Traceback (most recent call last): File "/usr/local/virtualenv-0.11/lib/python2.5/site-packages/Trac-0.11.7stable_r8997-py2.5.egg/trac/web/main.py", line 450, in _dispatch_request dispatcher.dispatch(req) File "/usr/local/virtualenv-0.11/lib/python2.5/site-packages/Trac-0.11.7stable_r8997-py2.5.egg/trac/web/main.py", line 227, in dispatch data, content_type) File "/usr/local/virtualenv-0.11/lib/python2.5/site-packages/Trac-0.11.7stable_r8997-py2.5.egg/trac/web/chrome.py", line 741, in render_template stream = template.generate(**data) File "build/bdist.linux-x86_64/egg/genshi/template/base.py", line 519, in generate stream = self.stream File "build/bdist.linux-x86_64/egg/genshi/template/base.py", line 429, in stream self._stream = list(self._prepare(self._stream)) File "build/bdist.linux-x86_64/egg/genshi/template/base.py", line 480, in _prepare for event in tmpl.stream: File "build/bdist.linux-x86_64/egg/genshi/template/base.py", line 429, in stream self._stream = list(self._prepare(self._stream)) File "build/bdist.linux-x86_64/egg/genshi/template/base.py", line 458, in _prepare for _, cls, value, namespaces, pos in sorted(data[0]): TypeError: 'StripDirective' object is not iterable
At that time, the Genshi version used was r1092.
Too bad I realized that so late, the access.log files from that period are long gone, so I have no clue which template triggered that error.
Change History (9)
comment:1 Changed 15 years ago by cboos
comment:2 follow-up: ↓ 3 Changed 15 years ago by cboos
- Keywords patch added
The Template._stream initialization was indeed not thread-safe.
The following change should fix this:
-
genshi/template/base.py
16 16 from collections import deque 17 17 import os 18 18 from StringIO import StringIO 19 try: 20 from threading import RLock 21 except ImportError: 22 from dummy_threading import RLock 19 23 import sys 20 24 21 25 from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure … … 368 372 369 373 serializer = None 370 374 _number_conv = unicode # function used to convert numbers to event data 375 _stream_lock = RLock() 371 376 372 377 def __init__(self, source, filepath=None, filename=None, loader=None, 373 378 encoding=None, lookup='strict', allow_exec=True): … … 439 444 @property 440 445 def stream(self): 441 446 if not self._prepared: 442 self._stream = list(self._prepare(self._stream)) 443 self._prepared = True 447 self._stream_lock.acquire() 448 try: 449 if not self._prepared: 450 self._stream = list(self._prepare(self._stream)) 451 self._prepared = True 452 finally: 453 self._stream_lock.release() 444 454 return self._stream 445 455 446 456 def _parse(self, source, encoding):
I used a global lock instead of a lock attached to the instance, for two reasons:
- no headaches with pickling
- avoid the possibility of a deadlock in case of two templates including each other(?)
comment:3 in reply to: ↑ 2 ; follow-up: ↓ 4 Changed 15 years ago by cmlenz
- Milestone changed from 0.7 to 0.6.1
- Status changed from new to assigned
Replying to cboos:
The Template._stream initialization was indeed not thread-safe.
The following change should fix this:
Thanks. Is the double check for self._prepared intentional?
comment:4 in reply to: ↑ 3 Changed 15 years ago by cboos
Replying to cmlenz:
Replying to cboos:
The Template._stream initialization was indeed not thread-safe.
The following change should fix this:
Thanks. Is the double check for self._prepared intentional?
Yes, the first check avoids blocking ever again once the stream is "_prepared", the second caters for a possible race condition if two or more threads happen to pass the if not self._prepared test during first time initialization.
There's even a name for this pattern, IIRC, seems to be wikipedia:"Double check locking". Now reading this I see many claims that this approach is supposedly broken but here it's different as we're testing a flag set after the assignment to self._stream and Python won't reorder write instructions I suppose... But maybe this question would be worth more digging. Doing only a check when the lock is held would bring us on the safe side, but I would certainly have a non-negligible performance cost (that global lock would have to be acquired on every call to stream) and for no reason if we can effectively rely on _prepared = True to happen after self._stream = list(...).
Reading again on this topic was useful, as it got me wary of using this approach with C++, so thanks for asking ;-)
comment:5 Changed 15 years ago by cboos
After reading more on the topic, I'm not that sure anymore about my initial patch. So the safer approach would be:
-
genshi/template/base.py
16 16 from collections import deque 17 17 import os 18 18 from StringIO import StringIO 19 try: 20 from threading import RLock 21 except ImportError: 22 from dummy_threading import RLock 19 23 import sys 20 24 21 25 from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure … … 368 372 369 373 serializer = None 370 374 _number_conv = unicode # function used to convert numbers to event data 375 _stream_lock = RLock() 371 376 372 377 def __init__(self, source, filepath=None, filename=None, loader=None, 373 378 encoding=None, lookup='strict', allow_exec=True): … … 438 443 439 444 @property 440 445 def stream(self): 441 if not self._prepared: 442 self._stream = list(self._prepare(self._stream)) 443 self._prepared = True 446 self._stream_lock.acquire() 447 try: 448 if not self._prepared: 449 self._stream = list(self._prepare(self._stream)) 450 self._prepared = True 451 finally: 452 self._stream_lock.release() 444 453 return self._stream 445 454 446 455 def _parse(self, source, encoding):
It doesn't seem to have a visible performance impact, at least not when running the test suite. It remains to be seen if this has an effect on a multithreaded Trac under load, but the measuring will not be easy.
comment:6 follow-up: ↓ 7 Changed 15 years ago by cboos
As in CPython the GIL ensures there's only one thread executing Python bytecode at a time and that a thread switch occur after a lock release/acquire, I think the first patch (in comment:2) should be safe.
But beyond that, I wonder if there couldn't be a safer and simpler approach, doing the _prepare explicitly after calling the callback(tmpl) in TemplateLoader.load. That would be done under the protection of the TemplateLoader's _lock. I guess the difficulty here is that we can't always assume there's a TemplateLoader involved, right?
comment:7 in reply to: ↑ 6 Changed 15 years ago by cboos
... doing the _prepare explicitly after calling the callback(tmpl) in TemplateLoader.load.
By which the fix becomes a one-liner:
-
genshi/template/loader.py
# svn diff
236 236 filename, encoding=encoding) 237 237 if self.callback: 238 238 self.callback(tmpl) 239 tmpl.stream # call _prepare in a thread-safe way 239 240 self._cache[cachekey] = tmpl 240 241 self._uptodate[cachekey] = uptodate 241 242 finally:
I guess the difficulty here is that we can't always assume there's a TemplateLoader involved, right?
... so we could simply state in the doc that using a Template from multiple threads needs some precautions:
- either the Template is obtained via a TemplateLoader and then it's safe
- or it's managed by hand, in which case the application should take care of preparing it in a thread-safe way, much like the TemplateLoader does
The advantage is that there's no overhead at all when the Template is purely local.
comment:8 Changed 10 years ago by rjollos
- Cc rjollos added
I find these errors in the trac-hacks.org logs a few times per week.
comment:9 Changed 8 years ago by hodgestar
- Milestone changed from 0.6.1 to 0.9
Move to milestone 0.9.
Another one:
From the timing and requests immediately preceding, this could correspond to:
Which should have returned the /about page.
Anyway, this was right after the morning restart, so this probably corresponds to a race. Immediately after that, there was the Timeout error... no connection I suppose, as we get Timeout daily even without the help of Genshi ;-)