Skip to content

Instantly share code, notes, and snippets.

@Jenjen1324
Last active May 23, 2024 13:42
Show Gist options
  • Save Jenjen1324/81d3547e5713ba253b67970bc8388147 to your computer and use it in GitHub Desktop.
Save Jenjen1324/81d3547e5713ba253b67970bc8388147 to your computer and use it in GitHub Desktop.
Odoo RPC request bug analysis

Odoo RPC request bug analysis

Issue description

Once a certain bad state is reached within the Odoo application (further explained later), API calls which trigger an automatic email will fail and result in the following stack trace:

Traceback (most recent call last):
  File "/odoo/src/odoo/http.py", line 1588, in _serve_db
    return service_model.retrying(self._serve_ir_http, self.env)
  File "/odoo/src/odoo/service/model.py", line 133, in retrying
    result = func()
  File "/odoo/src/odoo/http.py", line 1615, in _serve_ir_http
    response = self.dispatcher.dispatch(rule.endpoint, args)
  File "/odoo/src/odoo/http.py", line 1819, in dispatch
    result = self.request.registry['ir.http']._dispatch(endpoint)
  File "/odoo/external-src/odoo-cloud-platform/monitoring_prometheus/models/ir_http.py", line 38, in _dispatch
    res = super()._dispatch(endpoint)
  File "/odoo/src/odoo/addons/base/models/ir_http.py", line 154, in _dispatch
    result = endpoint(**request.params)
  File "/odoo/src/odoo/http.py", line 697, in route_wrapper
    result = endpoint(self, *args, **params_ok)
  File "/odoo/src/odoo/addons/base/controllers/rpc.py", line 158, in jsonrpc
    return dispatch_rpc(service, method, args)
  File "/odoo/src/odoo/http.py", line 366, in dispatch_rpc
    return dispatch(method, params)
  File "/odoo/src/odoo/service/model.py", line 37, in dispatch
    res = execute_kw(db, uid, *params[3:])
  File "/odoo/src/odoo/service/model.py", line 59, in execute_kw
    return execute(db, uid, obj, method, *args, **kw or {})
  File "/odoo/src/odoo/service/model.py", line 65, in execute
    res = execute_cr(cr, uid, obj, method, *args, **kw)
  File "/odoo/src/odoo/service/model.py", line 50, in execute_cr
    result = retrying(partial(odoo.api.call_kw, recs, method, args, kw), env)
  File "/odoo/src/odoo/service/model.py", line 133, in retrying
    result = func()
  File "/odoo/src/odoo/api.py", line 459, in call_kw
    result = _call_kw_model_create(method, model, args, kwargs)
  File "/odoo/src/odoo/api.py", line 439, in _call_kw_model_create
    result = method(recs, *args, **kwargs)
  File "/usr/local/lib/python3.9/dist-packages/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/odoo/src/odoo/api.py", line 409, in _model_create_multi
    return create(self, [arg])
  File "/odoo/src/addons/base_vat/models/res_partner.py", line 727, in create
    return super(ResPartner, self).create(vals_list)
  File "/usr/local/lib/python3.9/dist-packages/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/odoo/src/odoo/api.py", line 410, in _model_create_multi
    return create(self, arg)
  File "/odoo/src/addons/account/models/partner.py", line 625, in create
    return super().create(vals_list)
  File "/usr/local/lib/python3.9/dist-packages/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/odoo/src/odoo/api.py", line 410, in _model_create_multi
    return create(self, arg)
  File "/odoo/external-src/partner-contact/partner_firstname/models/res_partner.py", line 54, in create
    return super(ResPartner, self.with_context(context)).create(vals_list)
  File "/usr/local/lib/python3.9/dist-packages/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/odoo/src/odoo/api.py", line 410, in _model_create_multi
    return create(self, arg)
  File "/odoo/src/odoo/addons/base/models/res_partner.py", line 735, in create
    partners = super(Partner, self).create(vals_list)
  File "/usr/local/lib/python3.9/dist-packages/decorator.py", line 232, in fun
    return caller(func, *(extras + args), **kw)
  File "/odoo/src/odoo/api.py", line 410, in _model_create_multi
    return create(self, arg)
  File "/odoo/src/addons/mail/models/mail_thread.py", line 275, in create
    thread._message_auto_subscribe(create_values, followers_existing_policy='update')
  File "/odoo/src/addons/mail/models/mail_thread.py", line 3214, in _message_auto_subscribe
    self.with_context(lang=lang)._message_auto_subscribe_notify(pids, template)
  File "/odoo/src/addons/mail/models/mail_thread.py", line 3138, in _message_auto_subscribe_notify
    assignation_msg = self.env['ir.qweb']._render(template, values, minimal_qcontext=True)
  File "/odoo/src/odoo/tools/profiler.py", line 292, in _tracked_method_render
    return method_render(self, template, values, **options)
  File "/odoo/src/odoo/addons/base/models/ir_qweb.py", line 573, in _render
    irQweb = self.with_context(**options)._prepare_environment(values)
  File "/odoo/external-src/enterprise/web_studio/models/ir_qweb.py", line 39, in _prepare_environment
    return super()._prepare_environment(values)
  File "/odoo/src/addons/http_routing/models/ir_qweb.py", line 12, in _prepare_environment
    irQweb = super()._prepare_environment(values)
  File "/odoo/src/odoo/addons/base/models/ir_qweb.py", line 864, in _prepare_environment
    debug = request and request.session.debug or ''
  File "/usr/local/lib/python3.9/dist-packages/werkzeug/local.py", line 348, in __getattr__
    return getattr(self._get_current_object(), name)
AttributeError: 'Request' object has no attribute 'session'

Root cause analysis

The error is triggered by the following line of code:

odoo/src/odoo/addons/base/models/ir_qweb.py:864

    def _prepare_environment(self, values):
        """ Prepare the values and context that will sent to the
        compiled and evaluated function.

        :param values: template values to be used for rendering

        :returns self (with new context)
        """
        debug = request and request.session.debug or ''

This line, makes the assumption that request is either None or it is an object which contains a session attribute. It may never be a Request object without a session attribute. Yet this is the case.

Within ir_qweb.py the request object is imported directly from odoo.http where it is defined as follows:

# Thread local global request object
_request_stack = werkzeug.local.LocalStack()
request = _request_stack()

_request_stack = werkzeug.local.LocalStack creates a Thread-local stack object. The request object becomes a dynamic reference to the top element of this stack.

Note that during RPC-Calls borrow_request() is called, which removes the top element from _request_stack. Thus, at the location where the error occurs, we would expected request to be None - which doesn't seem to be the case. Another object must have been left on the _request_stack...

The only place where a request object is put on this stack is in odoo.http.Application.__call__:

request = Request(httprequest)
_request_stack.push(request)
request._post_init()
current_thread.url = httprequest.url

try:
    ...
finally:
    _request_stack.pop()

Now notice that Request.session is only set once Request._post_init has been called:

def _post_init(self):
    self.session, self.db = self._get_session_and_dbname()

Also notice that _request_stack.push(request) is called before request._post_init().

Also notice that _request_stack.pop() is only guaranteed to be called once we enter the try: statement

Which leads to the conclusion, should request._post_init() raise an exception, a Request object is left on the stack without having a defined Request.session. Which meets the pre-condition for the bug to be triggered.

Why would _post_init() raise an exception?

_post_init() calls Request._get_session_and_dbname. Analyzing the method in isolation, one could conclude that this function will never raise. Unless the session_store is replaced by something else.

As a matter of fact, the Odoo deployment of camp2camp replaces the session store with one backed by redis. Which my tests show, can raise an exception if redis becomes unreachable for example (possibly for other reasons as well). Therefore triggering this condition and leaving the Odoo instance permanently in this broken state until it is restarted.

Reproduction steps within a test environment

  1. Have a redis and mailhog running
  2. Set ODOO_SESSION_REDIS and ODOO_SESSION_REDIS_HOST to enable the redis session integration
  3. Set --workers=1. This causes the instance to use the production multiprocessing webserver. The gevent webserver used for testing will hide the issue, since it creates a new thread per request. As the request object is thread-local, any subsequent request will have a clean _request_stack in this case.
  4. Start the odoo instance and verify it's working
  5. Stop the redis service
  6. Run any API call and see that it errors
  7. Start the redis service
  8. Run the below API call to trigger the resulting issue
odoo.env['res.partner'].model.create({
    'name': 'Max Mustermann',
    'user_id': 104,
    'lang': 'de_DE',
    'invoice_communications': 'email',
    'firstname': 'Max',
    'lastname': 'Mustermann',
    'email': 'max@example.com'
})

Resolution methods

Make the code by Odoo failure tolerant

This is my preferred approach and I'm probably going to raise an issue about this myself in the odoo repo.

I have since submitted a PR to fix this issue: odoo/odoo#166524

If we simply move the following section of code into the try: section, the code would guarantee that _request_stack.pop() is called:

_request_stack.push(request)
request._post_init()
current_thread.url = httprequest.url

Make sure session_redis never ever raises an Exception

See: https://github.com/camptocamp/odoo-cloud-platform/tree/16.0/session_redis

This module would have to guarantee that it never ever raises an Exception for any reason... Which I find is more of a hack. If redis becomes unreachable it is a legitimate reason to raise.

Otherwise you may just restart odoo every time you notice an interruption in the connection to the redis server. Altough I'm not sure if redis being unreachable is the only possible reason for failure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment