Files
odoo_source/odoo/service/model.py
T
Julien Castiaux c0647b5c52 [REF] core: HTTPocalypse (14) changes all addons
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
2022-02-24 13:30:51 +00:00

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