[FIX] core: calling read_group / name_search over xmlrpc

Also non-browser jsonrpc (as it goes through a similar process): for
internal performance reasons, name_search and read_group have been
converted to a *lazy* name_get, so the "display name" is not
unnecessarily computed.

However this is an issue for the RPC endpoints (/xmlrpc and /jsonrpc)
as they have no support for `lazy` and thus tend to blow up and / or
do the wrong thing when trying to output a lazy:

* xmlrpc has no way to handle lazy at all and straight blows up
* jsonrpc falls back to `json_default` so they try to stringify the
  lazy, which might have worked except

*Problematically* both endpoints delegate the actual work to
`dispatch_rpc` which handles dispatching between various services and
ultimately creates a *new* cursor before calling model
methods (`object` service and `execute`/`execute_kw`).

This means by the time the result is serialized to be output, the
lazy's cursor has long been closed, and thus any access to an
unevaluated `lazy` errors out when trying to fetch the underlying
item.

This also means we can't just add a hook to serialize the lazy
in the xmlrpc marshaller, though we do have to do that. We *also* (for
both xmlrpc and jsonrpc) have to force evluation of lazy values before
our cursor is closed, meaning it has to be done right after the method
is invoked, iterating the entire response.

Related to task 2170343

closes odoo/odoo#49286

X-original-commit: e2b5a359c1d5eccbe725c1c3169b4130d7bca49b
Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
This commit is contained in:
Xavier Morel
2020-04-09 09:06:34 +00:00
parent 4a1bd8fe79
commit 1ecb0641ef
7 changed files with 78 additions and 18 deletions
+6
View File
@@ -7,6 +7,7 @@ from werkzeug.wrappers import Response
from odoo.http import Controller, dispatch_rpc, request, route
from odoo.service import wsgi_server
from odoo.fields import Date, Datetime
from odoo.tools import lazy
class OdooMarshaller(xmlrpc.client.Marshaller):
@@ -28,6 +29,11 @@ class OdooMarshaller(xmlrpc.client.Marshaller):
self.dump_unicode(value, write)
dispatch[date] = dump_date
def dump_lazy(self, value, write):
v = value._value
return self.dispatch[type(v)](self, v, write)
dispatch[lazy] = dump_lazy
# monkey-patch xmlrpc.client's marshaller
xmlrpc.client.Marshaller = OdooMarshaller
+39
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tools
from odoo.tests import common
@@ -25,3 +26,41 @@ class TestXMLRPC(common.HttpCase):
self.assertIsInstance(ids, list)
ids = o.execute(db_name, self.admin_uid, 'admin', 'ir.model', 'search', [], {})
self.assertIsInstance(ids, list)
def test_xmlrpc_read_group(self):
groups = self.xmlrpc_object.execute(
common.get_db_name(), self.admin_uid, 'admin',
'res.partner', 'read_group', [], ['is_company', 'color'], ['parent_id']
)
def test_xmlrpc_name_search(self):
self.xmlrpc_object.execute(
common.get_db_name(), self.admin_uid, 'admin',
'res.partner', 'name_search', "admin"
)
def test_jsonrpc_read_group(self):
self._json_call(
common.get_db_name(), self.admin_uid, 'admin',
'res.partner', 'read_group', [], ['is_company', 'color'], ['parent_id']
)
def test_jsonrpc_name_search(self):
# well that's some sexy sexy call right there
self._json_call(
common.get_db_name(),
self.admin_uid, 'admin',
'res.partner', 'name_search', 'admin'
)
def _json_call(self, *args):
self.opener.post("http://%s:%s/jsonrpc" % (common.HOST, odoo.tools.config['http_port']), json={
'jsonrpc': '2.0',
'id': None,
'method': 'call',
'params': {
'service': 'object',
'method': 'execute',
'args': args
}
})
@@ -6,7 +6,7 @@ import json
from odoo.addons.base.tests.common import SavepointCaseWithUserDemo
from odoo.tests.common import TransactionCase, users, warmup, tagged
from odoo.tools import mute_logger
from odoo.tools import mute_logger, json_default
class TestPerformance(SavepointCaseWithUserDemo):
@@ -447,7 +447,7 @@ class TestPerformance(SavepointCaseWithUserDemo):
self.assertEqual(res['value'], exp['value'])
# now serialize to json, which should force evaluation
with self.assertQueryCount(__system__=1, demo=1):
json.dumps(result)
json.dumps(result, default=json_default)
@tagged('bacon_and_eggs')
+7 -1
View File
@@ -15,6 +15,7 @@ from odoo.tools.translate import translate, translate_sql_constraint
from odoo.tools.translate import _
from . import security
from ..tools import traverse_containers, lazy
_logger = logging.getLogger(__name__)
@@ -155,7 +156,12 @@ def execute_cr(cr, uid, obj, method, *args, **kw):
recs = odoo.api.Environment(cr, uid, {}).get(obj)
if recs is None:
raise UserError(_("Object %s doesn't exist") % obj)
return odoo.api.call_kw(recs, method, args, kw)
result = odoo.api.call_kw(recs, method, args, kw)
# force evaluation of lazy values before the cursor is closed, as it would
# error afterwards if the lazy isn't already evaluated (and cached)
for l in traverse_containers(result, lazy):
_0 = l._value
return result
def execute_kw(db, uid, obj, method, args, kw=None):
+6 -4
View File
@@ -4,8 +4,9 @@ import calendar
from datetime import date, datetime, time
import pytz
from dateutil.relativedelta import relativedelta
from . import ustr
from . import ustr
from .func import lazy
def get_month(date):
''' Compute the month dates range on which the 'date' parameter belongs to.
@@ -198,16 +199,17 @@ def subtract(value, *args, **kwargs):
"""
return value - relativedelta(*args, **kwargs)
def json_default(obj):
"""
Properly serializes date and datetime objects.
"""
from odoo import fields
if isinstance(obj, datetime):
return fields.Datetime.to_string(obj)
if isinstance(obj, date):
if isinstance(obj, datetime):
return fields.Datetime.to_string(obj)
return fields.Date.to_string(obj)
if isinstance(obj, lazy):
return obj._value
return ustr(obj)
-11
View File
@@ -242,14 +242,3 @@ class lazy(object):
def __aenter__(self): return self._value.__aenter__()
def __aexit__(self, exc_type, exc_value, traceback):
return self._value.__aexit__(exc_type, exc_value, traceback)
# patch serialization of lazy
def default(self, o):
if isinstance(o, lazy):
return o._value
return json_encoder_default(self, o)
json_encoder_default = JSONEncoder.default
JSONEncoder.default = default
+18
View File
@@ -1507,3 +1507,21 @@ def get_diff(data_from, data_to, custom_style=False):
numlines=3,
)
return handle_style(diff, custom_style)
def traverse_containers(val, type_):
""" Yields atoms filtered by specified type_ (or type tuple), traverses
through standard containers (non-string mappings or sequences) *unless*
they're selected by the type filter
"""
if isinstance(val, type_):
yield val
elif isinstance(val, (str, bytes)):
return
elif isinstance(val, Mapping):
for k, v in val.items():
yield from traverse_containers(k, type_)
yield from traverse_containers(v, type_)
elif isinstance(val, collections.abc.Sequence):
for v in val:
yield from traverse_containers(v, type_)