This commit is the 14th commit of a comprehensive refactor of our HTTP framework. See odoo/odoo#78857 for complete historic, discussions and rationnals. * `request.uid = x` => `request.update_env(user=x)`. * `request.context = x` => `request.update_env(context=x)`. * `request.context = dict(request.context, x=y)` => `request.update_context(x=y)`. * `request.cr = None` => `request.cr.close()`. * `http.mono_db()` => `request.db`. * `http.dispatch_rpc()` => `service.dispatch_rpc()`. * `@service.model.check` => `service.model.retrying()`. * `request.endpoint` => `env['ir.http']._match(request.httprequest.path)[0].endpoint`. * `request.routing_iteration `=> `removed`. * `request.jsonrequest` => `request.dispatcher.jsonrequest`. Note that `request.params` is now set much later in the process. If you are in a situation where you values from the query string or the http body you can use `request.get_http_params()`. Note that using the new `request.future_response`, it is possible to add headers and cookies on the response object before the response object is initialized. Please note that headers/cookies saved on the future response will NOT be injected in case of error. PR: odoo#78857 Task: 2571224
171 lines
6.3 KiB
Python
171 lines
6.3 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from functools import partial
|
|
import json
|
|
import logging
|
|
from psycopg2 import IntegrityError, OperationalError, errorcodes
|
|
import random
|
|
import threading
|
|
import time
|
|
|
|
import odoo
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from odoo.http import request
|
|
from odoo.models import check_method_name
|
|
from odoo.tools import DotDict
|
|
from odoo.tools.translate import translate_sql_constraint
|
|
from odoo.tools.translate import _
|
|
|
|
from . import security
|
|
from ..tools import traverse_containers, lazy
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
PG_CONCURRENCY_ERRORS_TO_RETRY = (errorcodes.LOCK_NOT_AVAILABLE, errorcodes.SERIALIZATION_FAILURE, errorcodes.DEADLOCK_DETECTED)
|
|
MAX_TRIES_ON_CONCURRENCY_FAILURE = 5
|
|
|
|
|
|
def dispatch(method, params):
|
|
db, uid, passwd = params[0], int(params[1]), params[2]
|
|
security.check(db, uid, passwd)
|
|
|
|
threading.current_thread().dbname = db
|
|
threading.current_thread().uid = uid
|
|
registry = odoo.registry(db).check_signaling()
|
|
with registry.manage_changes():
|
|
if method == 'execute':
|
|
res = execute(db, uid, *params[3:])
|
|
elif method == 'execute_kw':
|
|
res = execute_kw(db, uid, *params[3:])
|
|
else:
|
|
raise NameError("Method not available %s" % method)
|
|
return res
|
|
|
|
|
|
def execute_cr(cr, uid, obj, method, *args, **kw):
|
|
# clean cache etc if we retry the same transaction
|
|
cr.reset()
|
|
env = odoo.api.Environment(cr, uid, {})
|
|
recs = env.get(obj)
|
|
if recs is None:
|
|
raise UserError(_("Object %s doesn't exist", obj))
|
|
result = retrying(partial(odoo.api.call_kw, recs, method, args, kw), env)
|
|
# 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):
|
|
return execute(db, uid, obj, method, *args, **kw or {})
|
|
|
|
|
|
def execute(db, uid, obj, method, *args, **kw):
|
|
with odoo.registry(db).cursor() as cr:
|
|
check_method_name(method)
|
|
res = execute_cr(cr, uid, obj, method, *args, **kw)
|
|
if res is None:
|
|
_logger.info('The method %s of the object %s can not return `None` !', method, obj)
|
|
return res
|
|
|
|
|
|
def _as_validation_error(env, exc):
|
|
""" Return the IntegrityError encapsuled in a nice ValidationError """
|
|
|
|
unknown = _('Unknown')
|
|
for _name, rclass in env.registry.items():
|
|
if exc.diag.table_name == rclass._table:
|
|
model = rclass
|
|
field = model._fields.get(exc.diag.column_name)
|
|
break
|
|
else:
|
|
model = DotDict({'_name': unknown.lower(), '_description': unknown})
|
|
field = DotDict({'name': unknown.lower(), 'string': unknown})
|
|
|
|
if exc.pgcode == errorcodes.NOT_NULL_VIOLATION:
|
|
return ValidationError(_(
|
|
"The operation cannot be completed:\n"
|
|
"- Create/update: a mandatory field is not set.\n"
|
|
"- Delete: another model requires the record being deleted."
|
|
" If possible, archive it instead.\n\n"
|
|
"Model: %(model_name)s (%(model_tech_name)s)\n"
|
|
"Field: %(field_name)s (%(field_tech_name)s)\n",
|
|
model_name=model._description,
|
|
model_tech_name=model._name,
|
|
field_name=field.string,
|
|
field_tech_name=field.name,
|
|
))
|
|
|
|
if exc.pgcode == errorcodes.FOREIGN_KEY_VIOLATION:
|
|
return ValidationError(_(
|
|
"The operation cannot be completed: another model requires "
|
|
"the record being deleted. If possible, archive it instead.\n\n"
|
|
"Model: %(model_name)s (%(model_tech_name)s)\n"
|
|
"Constraint: %(constraint)s\n",
|
|
model_name=model._description,
|
|
model_tech_name=model._name,
|
|
constraint=exc.diag.constraint_name,
|
|
))
|
|
|
|
if exc.diag.constraint_name in env.registry._sql_constraints:
|
|
return ValidationError(_(
|
|
"The operation cannot be completed: %s",
|
|
translate_sql_constraint(env.cr, exc.diag.constraint_name, env.context['lang'])
|
|
))
|
|
|
|
return ValidationError(_("The operation cannot be completed: %s", exc.args[0]))
|
|
|
|
|
|
def retrying(func, env):
|
|
"""
|
|
Call ``func`` until the function returns without serialisation
|
|
error. A serialisation error occurs when two requests in independent
|
|
cursors perform incompatible changes (such as writing different
|
|
values on a same record). By default, it retries up to 5 times.
|
|
|
|
:param callable func: The function to call, you can pass arguments
|
|
using :func:`functools.partial`:.
|
|
:param odoo.api.Environment env: The environment where the registry
|
|
and the cursor are taken.
|
|
"""
|
|
try:
|
|
for tryno in range(1, MAX_TRIES_ON_CONCURRENCY_FAILURE + 1):
|
|
tryleft = MAX_TRIES_ON_CONCURRENCY_FAILURE - tryno
|
|
try:
|
|
result = func()
|
|
if not env.cr._closed:
|
|
env.cr.flush() # submit the changes to the database
|
|
break
|
|
except (IntegrityError, OperationalError) as exc:
|
|
if env.cr._closed:
|
|
raise
|
|
env.cr.rollback()
|
|
env.registry.reset_changes()
|
|
if request:
|
|
request.session.clear()
|
|
request.session.update(json.loads(request.session.json_data))
|
|
if isinstance(exc, IntegrityError):
|
|
raise _as_validation_error(env, exc) from exc
|
|
if exc.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
|
|
raise
|
|
if not tryleft:
|
|
_logger.info("%s, maximum number of tries reached!", errorcodes.lookup(exc.pgcode))
|
|
raise
|
|
|
|
wait_time = random.uniform(0.0, 2 ** tryno)
|
|
_logger.info("%s, %s tries left, try again in %.04f sec...", errorcodes.lookup(exc.pgcode), tryleft, wait_time)
|
|
time.sleep(wait_time)
|
|
else:
|
|
# handled in the "if not tryleft" case
|
|
raise RuntimeError("unreachable")
|
|
|
|
except Exception:
|
|
env.registry.reset_changes()
|
|
raise
|
|
|
|
if not env.cr._closed:
|
|
env.cr.commit() # effectively commits and execute post-commits
|
|
env.registry.signal_changes()
|
|
return result
|