mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 10:52:05 +02:00
19.0 vanilla
This commit is contained in:
parent
0a7ae8db93
commit
991d2234ca
416 changed files with 646602 additions and 300844 deletions
|
|
@ -8,15 +8,19 @@ from functools import partial
|
|||
|
||||
from psycopg2 import IntegrityError, OperationalError, errorcodes, errors
|
||||
|
||||
import odoo
|
||||
from odoo.exceptions import UserError, ValidationError, AccessError
|
||||
from odoo import api, http
|
||||
from odoo.exceptions import (
|
||||
AccessDenied,
|
||||
AccessError,
|
||||
ConcurrencyError,
|
||||
UserError,
|
||||
ValidationError,
|
||||
)
|
||||
from odoo.models import BaseModel
|
||||
from odoo.http import request
|
||||
from odoo.modules.registry import Registry
|
||||
from odoo.tools import DotDict, lazy
|
||||
from odoo.tools.translate import translate_sql_constraint
|
||||
from odoo.tools import lazy
|
||||
|
||||
from . import security
|
||||
from .server import thread_local
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -25,127 +29,151 @@ PG_CONCURRENCY_EXCEPTIONS_TO_RETRY = (errors.LockNotAvailable, errors.Serializat
|
|||
MAX_TRIES_ON_CONCURRENCY_FAILURE = 5
|
||||
|
||||
|
||||
def get_public_method(model, name):
|
||||
class Params:
|
||||
"""Representation of parameters to a function call that can be stringified for display/logging"""
|
||||
def __init__(self, args, kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __str__(self):
|
||||
params = [repr(arg) for arg in self.args]
|
||||
params.extend(f"{key}={value!r}" for key, value in sorted(self.kwargs.items()))
|
||||
return ', '.join(params)
|
||||
|
||||
|
||||
def get_public_method(model: BaseModel, name: str):
|
||||
""" Get the public unbound method from a model.
|
||||
|
||||
When the method does not exist or is inaccessible, raise appropriate errors.
|
||||
Accessible methods are public (in sense that python defined it:
|
||||
not prefixed with "_") and are not decorated with `@api.private`.
|
||||
"""
|
||||
assert isinstance(model, BaseModel), f"{model!r} is not a BaseModel for {name}"
|
||||
assert isinstance(model, BaseModel)
|
||||
e = f"Private methods (such as '{model._name}.{name}') cannot be called remotely."
|
||||
if name.startswith('_'):
|
||||
raise AccessError(e)
|
||||
|
||||
cls = type(model)
|
||||
method = getattr(cls, name, None)
|
||||
if not callable(method):
|
||||
raise AttributeError(f"The method '{model._name}.{name}' does not exist") # noqa: TRY004
|
||||
|
||||
for mro_cls in cls.mro():
|
||||
cla_method = getattr(mro_cls, name, None)
|
||||
if not cla_method:
|
||||
if not (cla_method := getattr(mro_cls, name, None)):
|
||||
continue
|
||||
if name.startswith('_') or getattr(cla_method, '_api_private', False):
|
||||
raise AccessError(f"Private methods (such as '{model._name}.{name}') cannot be called remotely.")
|
||||
if getattr(cla_method, '_api_private', False):
|
||||
raise AccessError(e)
|
||||
|
||||
return method
|
||||
|
||||
|
||||
def call_kw(model: BaseModel, name: str, args: list, kwargs: Mapping):
|
||||
""" Invoke the given method ``name`` on the recordset ``model``.
|
||||
|
||||
Private methods cannot be called, only ones returned by `get_public_method`.
|
||||
"""
|
||||
method = get_public_method(model, name)
|
||||
|
||||
# get the records and context
|
||||
if getattr(method, '_api_model', False):
|
||||
# @api.model -> no ids
|
||||
recs = model
|
||||
else:
|
||||
ids, args = args[0], args[1:]
|
||||
recs = model.browse(ids)
|
||||
|
||||
# altering kwargs is a cause of errors, for instance when retrying a request
|
||||
# after a serialization error: the retry is done without context!
|
||||
kwargs = dict(kwargs)
|
||||
context = kwargs.pop('context', None) or {}
|
||||
recs = recs.with_context(context)
|
||||
|
||||
# call
|
||||
_logger.debug("call %s.%s(%s)", recs, method.__name__, Params(args, kwargs))
|
||||
result = method(recs, *args, **kwargs)
|
||||
|
||||
# adapt the result
|
||||
if name == "create":
|
||||
# special case for method 'create'
|
||||
result = result.id if isinstance(args[0], Mapping) else result.ids
|
||||
elif isinstance(result, BaseModel):
|
||||
result = result.ids
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def dispatch(method, params):
|
||||
db, uid, passwd = params[0], int(params[1]), params[2]
|
||||
security.check(db, uid, passwd)
|
||||
db, uid, passwd, model, method_, *args = params
|
||||
uid = int(uid)
|
||||
if not passwd:
|
||||
raise AccessDenied
|
||||
# access checked once we open a cursor
|
||||
|
||||
threading.current_thread().dbname = db
|
||||
threading.current_thread().uid = uid
|
||||
registry = Registry(db).check_signaling()
|
||||
with registry.manage_changes():
|
||||
try:
|
||||
if method == 'execute':
|
||||
res = execute(db, uid, *params[3:])
|
||||
kw = {}
|
||||
elif method == 'execute_kw':
|
||||
res = execute_kw(db, uid, *params[3:])
|
||||
# accept: (args, kw=None)
|
||||
if len(args) == 1:
|
||||
args += ({},)
|
||||
args, kw = args
|
||||
if kw is None:
|
||||
kw = {}
|
||||
else:
|
||||
raise NameError("Method not available %s" % method)
|
||||
raise NameError(f"Method not available {method}") # noqa: TRY301
|
||||
with registry.cursor() as cr:
|
||||
api.Environment(cr, api.SUPERUSER_ID, {})['res.users']._check_uid_passwd(uid, passwd)
|
||||
res = execute_cr(cr, uid, model, method_, args, kw)
|
||||
registry.signal_changes()
|
||||
except Exception:
|
||||
registry.reset_changes()
|
||||
raise
|
||||
return res
|
||||
|
||||
|
||||
def execute_cr(cr, uid, obj, method, *args, **kw):
|
||||
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, {})
|
||||
env = api.Environment(cr, uid, {})
|
||||
env.transaction.default_env = env # ensure this is the default env for the call
|
||||
recs = env.get(obj)
|
||||
if recs is None:
|
||||
raise UserError(env._("Object %s doesn't exist", obj))
|
||||
get_public_method(recs, method) # Don't use the result, call_kw will redo the getattr
|
||||
result = retrying(partial(odoo.api.call_kw, recs, method, args, kw), env)
|
||||
raise UserError(f"Object {obj} doesn't exist") # pylint: disable=missing-gettext
|
||||
thread_local.rpc_model_method = f'{obj}.{method}'
|
||||
result = retrying(partial(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
|
||||
if result is None:
|
||||
_logger.info('The method %s of the object %s cannot return `None`!', method, obj)
|
||||
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:
|
||||
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.
|
||||
Call ``func``in a loop until the SQL transaction commits with no
|
||||
serialisation error. It rollbacks the transaction in between calls.
|
||||
|
||||
A serialisation error occurs when two independent transactions
|
||||
attempt to commit incompatible changes such as writing different
|
||||
values on a same record. The first transaction to commit works, the
|
||||
second is canceled with a :class:`psycopg2.errors.SerializationFailure`.
|
||||
|
||||
This function intercepts those serialization errors, rollbacks the
|
||||
transaction, reset things that might have been modified, waits a
|
||||
random bit, and then calls the function again.
|
||||
|
||||
It calls the function up to ``MAX_TRIES_ON_CONCURRENCY_FAILURE`` (5)
|
||||
times. The time it waits between calls is random with an exponential
|
||||
backoff: ``random.uniform(0.0, 2 ** i)`` where ``i`` is the n° of
|
||||
the current attempt and starts at 1.
|
||||
|
||||
:param callable func: The function to call, you can pass arguments
|
||||
using :func:`functools.partial`:.
|
||||
using :func:`functools.partial`.
|
||||
:param odoo.api.Environment env: The environment where the registry
|
||||
and the cursor are taken.
|
||||
"""
|
||||
|
|
@ -157,12 +185,13 @@ def retrying(func, env):
|
|||
if not env.cr._closed:
|
||||
env.cr.flush() # submit the changes to the database
|
||||
break
|
||||
except (IntegrityError, OperationalError) as exc:
|
||||
except (IntegrityError, OperationalError, ConcurrencyError) as exc:
|
||||
if env.cr._closed:
|
||||
raise
|
||||
env.cr.rollback()
|
||||
env.reset()
|
||||
env.transaction.reset()
|
||||
env.registry.reset_changes()
|
||||
request = http.request
|
||||
if request:
|
||||
request.session = request._get_session_and_dbname()[0]
|
||||
# Rewind files in case of failure
|
||||
|
|
@ -172,22 +201,33 @@ def retrying(func, env):
|
|||
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):
|
||||
model = env['base']
|
||||
for rclass in env.registry.values():
|
||||
if exc.diag.table_name == rclass._table:
|
||||
model = env[rclass._name]
|
||||
break
|
||||
message = env._("The operation cannot be completed: %s", model._sql_error_to_message(exc))
|
||||
raise ValidationError(message) from exc
|
||||
|
||||
if isinstance(exc, PG_CONCURRENCY_EXCEPTIONS_TO_RETRY):
|
||||
error = errorcodes.lookup(exc.pgcode)
|
||||
elif isinstance(exc, ConcurrencyError):
|
||||
error = repr(exc)
|
||||
else:
|
||||
raise
|
||||
if not tryleft:
|
||||
_logger.info("%s, maximum number of tries reached!", errorcodes.lookup(exc.pgcode))
|
||||
_logger.info("%s, maximum number of tries reached!", error)
|
||||
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)
|
||||
_logger.info("%s, %s tries left, try again in %.04f sec...", error, tryleft, wait_time)
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
# handled in the "if not tryleft" case
|
||||
raise RuntimeError("unreachable")
|
||||
|
||||
except Exception:
|
||||
env.reset()
|
||||
env.transaction.reset()
|
||||
env.registry.reset_changes()
|
||||
raise
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue