[z3-five] Five traversal and acquisition
Walco van Loon
walco at infrae.com
Tue Aug 2 16:20:06 CEST 2005
Hi list,
I recently discovered that there are several issues with Five traversal
and acquisition.
The most obvious problem with Five traversal is that an (un)restricted
traverse (through a Five traversable) to a non-existing attribute is
done three times, by Zope 3 traversal, Five, and by Zope 2 traversal:
try:
return ITraverser(self).traverse(
path=[name], request=REQUEST).__of__(self)
except (ComponentLookupError, NotFoundError,
AttributeError, KeyError, NotFound):
pass
The NotFoundError raised by Zope 3 traversal is caught silently, then
the Five's __bobo_traverse__ does an getattr and a getitem. That fails
too, after which the Zope 2 traversal does the attribute lookup again. I
suggest that the FiveTraversable should only do a view lookup and not
call the traverse method of its superclass.
All tests still pass if the call to Zope 3's DefaultTraversable traverse
method is removed, so I'd like to fix this in svn.
This fix also seems to solve a much less obvious problem with Five
traversal which I'd like to discuss here - it could be that the fix
above only fixes the symptoms of a more fundamental problem with Five
and acquisition. As I'm not an expert on Zope acquisition, I'll try to
describe the problem below.
The problem is that traversal time of Five traversables (having a
monkey-patched __bobo_traverse_ method), with attributes that make use
of implicit acquisition, can have near exponential characteristics if
the last attribute in the path doesn't exist.
To demonstrate this phenomenon, consider the following url (somecontent
is a Five traversable, someattr is an object that extends
Acquisition.Implicit).
/somefolder/somecontent/someattr/somecontent/someattr/somecontent/someattr
/somecontent/someattr/404
(and yes, variations on this kind of urls can be found in the wild.
Don't ask ;-))
Zope 2 recognizes repeated attributes and objects in acquisition
structures to avoid repeated attribute lookups, which explains why urls
like this don't cause problems in 'pure Zope 2' traversal, but Five
seems to frustrate this process.
By the time the last someattr on the path is searched for '404' (in Zope
2's unrestrictedtraverse), the object returned by the Traverser adapter
seems to have built up a huge and complex acquisition structure, because
it's this getattr call in Zope 2's unrestrictedtraverse that takes
increasingly more time with each segment that is added to the traversal
path.
A reason for this, but I'm guessing here, could be that each time
somecontent is traversed, its patched __bobo_traverse__ is called, and a
wrapped traversal result is returned. The repeated wrapping of the
traversal result by Five seems to frustrate Zope 2's acquisition short
circuiting.
The problem may seem a bit theoretical, but since the attribute lookups
are done in Acquisiton C code, which causes all other threads to wait
for the interpreter lock, the result is that if urls like this get long
enough, they can effectively 'hang' the entire Zope instance.
I've attached a test that demonstrates the problem.
Cheers,
Walco
-------------- next part --------------
##############################################################################
#
# Copyright (c) 2004, 2005 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Test Five-traversable classes
$Id: test_traversable.py 14595 2005-07-12 21:26:12Z philikon $
"""
import os, sys
if __name__ == '__main__':
execfile(os.path.join(sys.path[0], 'framework.py'))
def test_traversable():
"""
Test the behaviour of Five-traversable classes.
>>> import Products.Five
>>> from Products.Five import zcml
>>> zcml.load_config("configure.zcml", Products.Five)
``SimpleContent`` is a traversable class by default. Its fallback
traverser should raise NotFound when traversal fails. (Note: If
we return None in __fallback_traverse__, this test passes but for
the wrong reason: None doesn't have a docstring so BaseRequest
raises NotFoundError.)
>>> from Products.Five.testing.simplecontent import manage_addSimpleContent
>>> manage_addSimpleContent(self.folder, 'testoid', 'Testoid')
>>> print http(r'''
... GET /test_folder_1_/testoid/doesntexist HTTP/1.1
... ''')
HTTP/1.1 404 Not Found
...
Now let's take class which already has a __bobo_traverse__ method.
Five should correctly use that as a fallback.
>>> configure_zcml = '''
... <configure xmlns="http://namespaces.zope.org/zope"
... xmlns:browser="http://namespaces.zope.org/browser"
... xmlns:five="http://namespaces.zope.org/five">
...
... <!-- make the zope2.Public permission work -->
... <redefinePermission from="zope2.Public" to="zope.Public" />
...
... <five:traversable
... class="Products.Five.testing.fancycontent.FancyContent"
... />
...
... <browser:page
... for="Products.Five.testing.fancycontent.IFancyContent"
... class="Products.Five.browser.tests.pages.FancyView"
... attribute="view"
... name="fancy"
... permission="zope2.Public"
... />
...
... </configure>'''
>>> zcml.load_string(configure_zcml)
>>> from Products.Five.testing.fancycontent import manage_addFancyContent
>>> info = manage_addFancyContent(self.folder, 'fancy', '')
In the following test we let the original __bobo_traverse__ method
kick in:
>>> print http(r'''
... GET /test_folder_1_/fancy/something-else HTTP/1.1
... ''')
HTTP/1.1 200 OK
...
something-else
Of course we also need to make sure that Zope 3 style view lookup
actually works:
>>> print http(r'''
... GET /test_folder_1_/fancy/fancy HTTP/1.1
... ''')
HTTP/1.1 200 OK
...
Fancy, fancy
Testing borked url traversal.
>>> import Acquisition
>>> class Acquiring(Acquisition.Implicit):
... exist = 1
>>> class NotAcquiring:
... def __init__(self): pass
... def __getitem__(self, name):
... if name == '404': raise AttributeError, name
... return self
>>> setattr(self.folder.testoid, 'impl_acq', Acquiring())
>>> segment_pairs = 15
>>> url = '/test_folder_1_' + '/testoid/impl_acq' * segment_pairs + '/404'
>>> self.folder.unrestrictedTraverse(url)
Traceback (most recent call last):
NotFound: 404
>>> url = '/test_folder_1_' + '/testoid/impl_acq' * segment_pairs + '/exist'
>>> self.folder.unrestrictedTraverse(url)
1
This case shows that the problem only occurs when acquisition is
involved:
>>> setattr(self.folder.testoid, 'no_acq', NotAcquiring())
>>> url = '/test_folder_1_' + '/testoid/no_acq' * segment_pairs + '/404'
>>> self.folder.unrestrictedTraverse(url)
Traceback (most recent call last):
NotFound: 404
Clean up:
>>> from zope.app.tests.placelesssetup import tearDown
>>> tearDown()
"""
def test_suite():
from Testing.ZopeTestCase import FunctionalDocTestSuite
return FunctionalDocTestSuite()
if __name__ == '__main__':
framework()
More information about the z3-five
mailing list