From abde3e0e5b4ea148889d4e6adf953f64ceb83928 Mon Sep 17 00:00:00 2001 From: Olivier Dony <odo@openerp.com> Date: Fri, 11 Sep 2015 11:51:28 +0200 Subject: [PATCH] [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. --- openerp/addons/base/ir/ir_qweb.py | 10 +--------- openerp/exceptions.py | 13 ++++++++++++- openerp/http.py | 11 ++++++++--- openerp/service/model.py | 10 ++++++++-- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 29d69f096928..2811521ec329 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -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 diff --git a/openerp/exceptions.py b/openerp/exceptions.py index 209f2a4cff28..8d0bb779d3e6 100644 --- a/openerp/exceptions.py +++ b/openerp/exceptions.py @@ -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) diff --git a/openerp/http.py b/openerp/http.py index cd608492f1a5..1d5226d3430a 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -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): diff --git a/openerp/service/model.py b/openerp/service/model.py index d1a2d103cd68..decfd5d8ba11 100644 --- a/openerp/service/model.py +++ b/openerp/service/model.py @@ -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 -- GitLab