[Zope-Checkins] SVN: Zope/trunk/lib/python/ZPublisher/ dd Michael
Dunstan's "explicit abort after error propagates into
publisher" patch as documented in
http://zope.org/Collectors/Zope/789 and as required by recent
changes to ZODB which prevent a connection from being cleanly
closed if modifications are extant in that connection.
Chris McDonough
chrism at plope.com
Sat Sep 4 23:05:43 EDT 2004
Log message for revision 27449:
dd Michael Dunstan's "explicit abort after error propagates into publisher" patch as documented in http://zope.org/Collectors/Zope/789 and as required by recent changes to ZODB which prevent a connection from being cleanly closed if modifications are extant in that connection.
(Merge from 2.7 branch)
Changed:
U Zope/trunk/lib/python/ZPublisher/Publish.py
A Zope/trunk/lib/python/ZPublisher/tests/testPublish.py
-=-
Modified: Zope/trunk/lib/python/ZPublisher/Publish.py
===================================================================
--- Zope/trunk/lib/python/ZPublisher/Publish.py 2004-09-04 04:46:34 UTC (rev 27448)
+++ Zope/trunk/lib/python/ZPublisher/Publish.py 2004-09-05 03:05:43 UTC (rev 27449)
@@ -136,36 +136,31 @@
if parents:
parents=parents[0]
try:
- response = err_hook(parents, request,
+ try:
+ return err_hook(parents, request,
sys.exc_info()[0],
sys.exc_info()[1],
sys.exc_info()[2],
)
- if transactions_manager:
- transactions_manager.abort()
- return response
-
- except Retry:
- if not request.supports_retry():
- response = err_hook(parents, request,
+ except Retry:
+ if not request.supports_retry():
+ return err_hook(parents, request,
sys.exc_info()[0],
sys.exc_info()[1],
sys.exc_info()[2],
)
- if transactions_manager:
- transactions_manager.abort()
- return response
-
-
+ finally:
if transactions_manager:
transactions_manager.abort()
- newrequest=request.retry()
- request.close() # Free resources held by the request.
- try:
- return publish(newrequest, module_name, after_list, debug)
- finally:
- newrequest.close()
+ # Only reachable if Retry is raised and request supports retry.
+ newrequest=request.retry()
+ request.close() # Free resources held by the request.
+ try:
+ return publish(newrequest, module_name, after_list, debug)
+ finally:
+ newrequest.close()
+
else:
if transactions_manager:
transactions_manager.abort()
Added: Zope/trunk/lib/python/ZPublisher/tests/testPublish.py
===================================================================
--- Zope/trunk/lib/python/ZPublisher/tests/testPublish.py 2004-09-04 04:46:34 UTC (rev 27448)
+++ Zope/trunk/lib/python/ZPublisher/tests/testPublish.py 2004-09-05 03:05:43 UTC (rev 27449)
@@ -0,0 +1,273 @@
+from ZPublisher import Retry
+from ZODB.POSException import ConflictError
+
+class Tracer:
+ """Trace used to record pathway taken through the publisher
+ machinery. And provide framework for spewing out exceptions at
+ just the right time.
+ """
+
+ def __init__(self):
+ self.reset()
+
+ def reset(self):
+ self.tracedPath = []
+ self.exceptions = {}
+
+ def append(self, arg):
+ self.tracedPath.append(arg)
+
+ def showTracedPath(self):
+ for arg in self.tracedPath:
+ print arg
+
+ def possiblyRaiseException(self, context):
+ exceptions = tracer.exceptions.get(context, None)
+ if exceptions:
+ exception = exceptions[0]
+ exceptions.remove(exception)
+ exceptionShortName = str(exception).split('.')[-1]
+ self.append('raising %s from %s' % (exceptionShortName, context))
+ raise exception
+
+
+tracer = Tracer()
+
+class TransactionsManager:
+ """Mock TransactionManager to replace
+ Zope.App.startup.TransactionsManager.
+ """
+
+ def abort(self):
+ tracer.append('abort')
+
+ def begin(self):
+ tracer.append('begin')
+
+ def commit(self):
+ tracer.append('commit')
+ tracer.possiblyRaiseException('commit')
+
+ def recordMetaData(self, obj, request):
+ pass
+
+zpublisher_transactions_manager = TransactionsManager()
+
+def zpublisher_exception_hook(published, request, t, v, traceback):
+ """Mock zpublisher_exception_hook to replace
+ Zope.App.startup.zpublisher_exception_hook
+ """
+
+ if issubclass(t, ConflictError):
+ raise Retry(t, v, traceback)
+ if t is Retry:
+ v.reraise()
+ tracer.append('zpublisher_exception_hook')
+ tracer.possiblyRaiseException('zpublisher_exception_hook')
+ return 'zpublisher_exception_hook'
+
+class Object:
+ """Mock object for traversing to.
+ """
+
+ def __call__(self):
+ tracer.append('__call__')
+ tracer.possiblyRaiseException('__call__')
+ return '__call__'
+
+class Response:
+ """Mock Response to replace ZPublisher.HTTPResponse.HTTPResponse.
+ """
+
+ def setBody(self, a):
+ pass
+
+class Request:
+ """Mock Request to replace ZPublisher.HTTPRequest.HTTPRequest.
+ """
+
+ args = ()
+
+ def __init__(self):
+ self.response = Response()
+
+ def processInputs(self):
+ pass
+
+ def get(self, a, b=''):
+ return ''
+
+ def __setitem__(self, name, value):
+ pass
+
+ def traverse(self, path, validated_hook):
+ return Object()
+
+ def close(self):
+ pass
+
+ retry_count = 0
+ retry_max_count = 3
+
+ def supports_retry(self):
+ return self.retry_count < self.retry_max_count
+
+ def retry(self):
+ self.retry_count += 1
+ r = self.__class__()
+ r.retry_count = self.retry_count
+ return r
+
+module_name = __name__
+after_list = [None]
+
+
+def testPublisher():
+ """
+ Tests to ensure that the ZPublisher correctly manages the ZODB
+ transaction boundaries.
+
+ >>> from ZPublisher.Publish import publish
+
+ ZPublisher will commit the transaction after it has made a
+ rendering of the object.
+
+ >>> tracer.reset()
+ >>> request = Request()
+ >>> response = publish(request, module_name, after_list)
+ >>> tracer.showTracedPath()
+ begin
+ __call__
+ commit
+
+ If ZPublisher sees an exception when rendering the requested
+ object then it will try rendering an error message. The
+ transaction is eventually aborted after rendering the error
+ message. (Note that this handling of the transaction boundaries is
+ different to how Zope3 does things. Zope3 aborts the transaction
+ before rendering the error message.)
+
+ >>> tracer.reset()
+ >>> tracer.exceptions['__call__'] = [ValueError]
+ >>> request = Request()
+ >>> response = publish(request, module_name, after_list)
+ >>> tracer.showTracedPath()
+ begin
+ __call__
+ raising ValueError from __call__
+ zpublisher_exception_hook
+ abort
+
+ If there is a futher exception raised while trying to render the
+ error then ZPublisher is still required to abort the
+ transaction. And the exception propagates out of publish.
+
+ >>> tracer.reset()
+ >>> tracer.exceptions['__call__'] = [ValueError]
+ >>> tracer.exceptions['zpublisher_exception_hook'] = [ValueError]
+ >>> request = Request()
+ >>> response = publish(request, module_name, after_list)
+ Traceback (most recent call last):
+ ...
+ ValueError
+ >>> tracer.showTracedPath()
+ begin
+ __call__
+ raising ValueError from __call__
+ zpublisher_exception_hook
+ raising ValueError from zpublisher_exception_hook
+ abort
+
+ ZPublisher can also deal with database ConflictErrors. The original
+ transaction is aborted and a second is made in which the request
+ is attempted again. (There is a fair amount of collaboration to
+ implement the retry functionality. Relies on Request and
+ zpublisher_exception_hook also doing the right thing.)
+
+ >>> tracer.reset()
+ >>> tracer.exceptions['__call__'] = [ConflictError]
+ >>> request = Request()
+ >>> response = publish(request, module_name, after_list)
+ >>> tracer.showTracedPath()
+ begin
+ __call__
+ raising ConflictError from __call__
+ abort
+ begin
+ __call__
+ commit
+
+ Same behaviour if there is a conflict when attempting to commit
+ the transaction. (Again this relies on collaboration from
+ zpublisher_exception_hook.)
+
+ >>> tracer.reset()
+ >>> tracer.exceptions['commit'] = [ConflictError]
+ >>> request = Request()
+ >>> response = publish(request, module_name, after_list)
+ >>> tracer.showTracedPath()
+ begin
+ __call__
+ commit
+ raising ConflictError from commit
+ abort
+ begin
+ __call__
+ commit
+
+ ZPublisher will retry the request several times. After 3 retries it
+ gives up and the exception propogates out.
+
+ >>> tracer.reset()
+ >>> tracer.exceptions['__call__'] = [ConflictError, ConflictError,
+ ... ConflictError, ConflictError]
+ >>> request = Request()
+ >>> response = publish(request, module_name, after_list)
+ Traceback (most recent call last):
+ ...
+ ConflictError: database conflict error
+ >>> tracer.showTracedPath()
+ begin
+ __call__
+ raising ConflictError from __call__
+ abort
+ begin
+ __call__
+ raising ConflictError from __call__
+ abort
+ begin
+ __call__
+ raising ConflictError from __call__
+ abort
+ begin
+ __call__
+ raising ConflictError from __call__
+ abort
+
+ However ZPublisher does not retry ConflictErrors that are raised
+ while trying to render an error message.
+
+ >>> tracer.reset()
+ >>> tracer.exceptions['__call__'] = [ValueError]
+ >>> tracer.exceptions['zpublisher_exception_hook'] = [ConflictError]
+ >>> request = Request()
+ >>> response = publish(request, module_name, after_list)
+ Traceback (most recent call last):
+ ...
+ ConflictError: database conflict error
+ >>> tracer.showTracedPath()
+ begin
+ __call__
+ raising ValueError from __call__
+ zpublisher_exception_hook
+ raising ConflictError from zpublisher_exception_hook
+ abort
+
+ """
+ pass
+
+
+import doctest
+
+def test_suite():
+ return doctest.DocTestSuite()
More information about the Zope-Checkins
mailing list