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'
The error is triggered by the following line of code:
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.
_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.
- Have a redis and mailhog running
- Set
ODOO_SESSION_REDIS
andODOO_SESSION_REDIS_HOST
to enable the redis session integration - 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. - Start the odoo instance and verify it's working
- Stop the redis service
- Run any API call and see that it errors
- Start the redis service
- 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'
})
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.