|
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
- import logging
- import random
- import threading
- import time
- from collections.abc import Mapping, Sequence
- from functools import partial
-
- from psycopg2 import IntegrityError, OperationalError, errorcodes, errors
-
- import odoo
- from odoo.exceptions import UserError, ValidationError
- from odoo.http import request
- from odoo.models import check_method_name
- from odoo.modules.registry import Registry
- from odoo.tools import DotDict, lazy
- from odoo.tools.translate import translate_sql_constraint
-
- from . import security
-
- _logger = logging.getLogger(__name__)
-
- PG_CONCURRENCY_ERRORS_TO_RETRY = (errorcodes.LOCK_NOT_AVAILABLE, errorcodes.SERIALIZATION_FAILURE, errorcodes.DEADLOCK_DETECTED)
- PG_CONCURRENCY_EXCEPTIONS_TO_RETRY = (errors.LockNotAvailable, errors.SerializationFailure, errors.DeadlockDetected)
- 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 = 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(env._("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):
- # TODO could be conditionnaly readonly as in _call_kw_readonly
- with 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 = env._('Unknown')
- model = DotDict({'_name': 'unknown', '_description': unknown})
- field = DotDict({'name': 'unknown', 'string': 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) or field
- break
-
- match exc:
- case errors.NotNullViolation():
- return ValidationError(env._(
- "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,
- ))
-
- case errors.ForeignKeyViolation():
- return ValidationError(env._(
- "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(env._(
- "The operation cannot be completed: %s",
- translate_sql_constraint(env.cr, exc.diag.constraint_name, env.context.get('lang', 'en_US'))
- ))
-
- return ValidationError(env._("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.reset()
- env.registry.reset_changes()
- if request:
- request.session = request._get_session_and_dbname()[0]
- # Rewind files in case of failure
- for filename, file in request.httprequest.files.items():
- if hasattr(file, "seekable") and file.seekable():
- file.seek(0)
- else:
- raise RuntimeError(f"Cannot retry request on input file {filename!r} after serialization failure") from exc
- if isinstance(exc, IntegrityError):
- raise _as_validation_error(env, exc) from exc
- if not isinstance(exc, PG_CONCURRENCY_EXCEPTIONS_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.reset()
- 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
-
-
- 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
- """
- from odoo.models import BaseModel
- if isinstance(val, type_):
- yield val
- elif isinstance(val, (str, bytes, BaseModel)):
- 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, Sequence):
- for v in val:
- yield from _traverse_containers(v, type_)
|