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