[IMP] http: LazyResponse rendering w/ concurrent access protection

QWeb's LazyResponse mechanism allows controllers to be inherited
and to alter the rendering context (qwebcontext) *after* calling
super().

However it caused the final rendering to occur outside the protection
of the @service.model.check decorator that handles concurrent access
errors. In rare cases the lazy rendering can be interrupted by
a concurrent access error, for example when bootstrapping the
the cache of asset bundles (as it INSERTs a bunch of attachments).

By forcing the flatten()ing of the lazy response earlier, right
after calling the route's endpoint(), it occurs within the
protection of the @service.model.check, and benefits from the
automatic retry.

flatten() was modified to avoid rendering twice the template
The flatten() in get_response() is still necessary for addons
code that directly calls get_response().

Moved QWebException into openerp.exceptions next to the other
exceptions classes - since it now has more global semantics.

~
The whole request dispatching code could really do with a
complete redesign/simplification.
This commit is contained in:
Olivier Dony
2015-09-11 14:15:57 +02:00
parent b9420fdea8
commit abde3e0e5b
4 changed files with 29 additions and 15 deletions
+1 -9
View File
@@ -27,6 +27,7 @@ import openerp.http
import openerp.tools
from openerp.tools.func import lazy_property
import openerp.tools.lru
from openerp.exceptions import QWebException
from openerp.fields import Datetime
from openerp.http import request
from openerp.tools.safe_eval import safe_eval as eval
@@ -43,15 +44,6 @@ MAX_CSS_RULES = 4095
#--------------------------------------------------------------------
# QWeb template engine
#--------------------------------------------------------------------
class QWebException(Exception):
def __init__(self, message, **kw):
Exception.__init__(self, message)
self.qweb = dict(kw)
def pretty_xml(self):
if 'node' not in self.qweb:
return ''
return etree.tostring(self.qweb['node'], pretty_print=True)
class QWebTemplateNotFound(QWebException):
pass
+12 -1
View File
@@ -10,8 +10,9 @@ treated as a 'Server error'.
If you consider introducing new exceptions, check out the test_exceptions addon.
"""
from inspect import currentframe
import logging
from inspect import currentframe
from lxml import etree
from tools.func import frame_codeinfo
_logger = logging.getLogger(__name__)
@@ -90,3 +91,13 @@ class DeferredException(Exception):
def __init__(self, msg, tb):
self.message = msg
self.traceback = tb
class QWebException(Exception):
def __init__(self, message, **kw):
super(QWebException, self).__init__(message)
self.qweb = dict(kw)
def pretty_xml(self):
if 'node' not in self.qweb:
return ''
return etree.tostring(self.qweb['node'], pretty_print=True)
+8 -3
View File
@@ -306,7 +306,11 @@ class WebRequest(object):
# case, the request cursor is unusable. Rollback transaction to create a new one.
if self._cr:
self._cr.rollback()
return self.endpoint(*a, **kw)
result = self.endpoint(*a, **kw)
if isinstance(result, Response) and result.is_qweb:
# Early rendering of lazy responses to benefit from @service_model.check protection
result.flatten()
return result
if self.db:
return checked_call(self.db, *args, **kwargs)
@@ -1299,8 +1303,9 @@ class Response(werkzeug.wrappers.Response):
""" Forces the rendering of the response's template, sets the result
as response body and unsets :attr:`.template`
"""
self.response.append(self.render())
self.template = None
if self.template:
self.response.append(self.render())
self.template = None
class DisableCacheMiddleware(object):
def __init__(self, app):
+8 -2
View File
@@ -8,7 +8,7 @@ import threading
import time
import openerp
from openerp.exceptions import UserError, ValidationError
from openerp.exceptions import UserError, ValidationError, QWebException
from openerp.tools.translate import translate
from openerp.tools.translate import _
@@ -111,7 +111,13 @@ def check(f):
if openerp.registry(dbname)._init and not openerp.tools.config['test_enable']:
raise openerp.exceptions.Warning('Currently, this database is not fully loaded and can not be used.')
return f(dbname, *args, **kwargs)
except OperationalError, e:
except (OperationalError, QWebException) as e:
if isinstance(e, QWebException):
cause = e.qweb.get('cause')
if isinstance(cause, OperationalError):
e = cause
else:
raise
# Automatically retry the typical transaction serialization errors
if e.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
raise