model.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. import logging
  3. import random
  4. import threading
  5. import time
  6. from collections.abc import Mapping, Sequence
  7. from functools import partial
  8. from psycopg2 import IntegrityError, OperationalError, errorcodes
  9. import odoo
  10. from odoo.exceptions import UserError, ValidationError
  11. from odoo.http import request
  12. from odoo.models import check_method_name
  13. from odoo.tools import DotDict
  14. from odoo.tools.translate import _, translate_sql_constraint
  15. from . import security
  16. from ..tools import lazy
  17. _logger = logging.getLogger(__name__)
  18. PG_CONCURRENCY_ERRORS_TO_RETRY = (errorcodes.LOCK_NOT_AVAILABLE, errorcodes.SERIALIZATION_FAILURE, errorcodes.DEADLOCK_DETECTED)
  19. MAX_TRIES_ON_CONCURRENCY_FAILURE = 5
  20. def dispatch(method, params):
  21. db, uid, passwd = params[0], int(params[1]), params[2]
  22. security.check(db, uid, passwd)
  23. threading.current_thread().dbname = db
  24. threading.current_thread().uid = uid
  25. registry = odoo.registry(db).check_signaling()
  26. with registry.manage_changes():
  27. if method == 'execute':
  28. res = execute(db, uid, *params[3:])
  29. elif method == 'execute_kw':
  30. res = execute_kw(db, uid, *params[3:])
  31. else:
  32. raise NameError("Method not available %s" % method)
  33. return res
  34. def execute_cr(cr, uid, obj, method, *args, **kw):
  35. # clean cache etc if we retry the same transaction
  36. cr.reset()
  37. env = odoo.api.Environment(cr, uid, {})
  38. recs = env.get(obj)
  39. if recs is None:
  40. raise UserError(_("Object %s doesn't exist", obj))
  41. result = retrying(partial(odoo.api.call_kw, recs, method, args, kw), env)
  42. # force evaluation of lazy values before the cursor is closed, as it would
  43. # error afterwards if the lazy isn't already evaluated (and cached)
  44. for l in _traverse_containers(result, lazy):
  45. _0 = l._value
  46. return result
  47. def execute_kw(db, uid, obj, method, args, kw=None):
  48. return execute(db, uid, obj, method, *args, **kw or {})
  49. def execute(db, uid, obj, method, *args, **kw):
  50. with odoo.registry(db).cursor() as cr:
  51. check_method_name(method)
  52. res = execute_cr(cr, uid, obj, method, *args, **kw)
  53. if res is None:
  54. _logger.info('The method %s of the object %s can not return `None` !', method, obj)
  55. return res
  56. def _as_validation_error(env, exc):
  57. """ Return the IntegrityError encapsuled in a nice ValidationError """
  58. unknown = _('Unknown')
  59. model = DotDict({'_name': unknown.lower(), '_description': unknown})
  60. field = DotDict({'name': unknown.lower(), 'string': unknown})
  61. for _name, rclass in env.registry.items():
  62. if exc.diag.table_name == rclass._table:
  63. model = rclass
  64. field = model._fields.get(exc.diag.column_name) or field
  65. break
  66. if exc.pgcode == errorcodes.NOT_NULL_VIOLATION:
  67. return ValidationError(_(
  68. "The operation cannot be completed:\n"
  69. "- Create/update: a mandatory field is not set.\n"
  70. "- Delete: another model requires the record being deleted."
  71. " If possible, archive it instead.\n\n"
  72. "Model: %(model_name)s (%(model_tech_name)s)\n"
  73. "Field: %(field_name)s (%(field_tech_name)s)\n",
  74. model_name=model._description,
  75. model_tech_name=model._name,
  76. field_name=field.string,
  77. field_tech_name=field.name,
  78. ))
  79. if exc.pgcode == errorcodes.FOREIGN_KEY_VIOLATION:
  80. return ValidationError(_(
  81. "The operation cannot be completed: another model requires "
  82. "the record being deleted. If possible, archive it instead.\n\n"
  83. "Model: %(model_name)s (%(model_tech_name)s)\n"
  84. "Constraint: %(constraint)s\n",
  85. model_name=model._description,
  86. model_tech_name=model._name,
  87. constraint=exc.diag.constraint_name,
  88. ))
  89. if exc.diag.constraint_name in env.registry._sql_constraints:
  90. return ValidationError(_(
  91. "The operation cannot be completed: %s",
  92. translate_sql_constraint(env.cr, exc.diag.constraint_name, env.context.get('lang', 'en_US'))
  93. ))
  94. return ValidationError(_("The operation cannot be completed: %s", exc.args[0]))
  95. def retrying(func, env):
  96. """
  97. Call ``func`` until the function returns without serialisation
  98. error. A serialisation error occurs when two requests in independent
  99. cursors perform incompatible changes (such as writing different
  100. values on a same record). By default, it retries up to 5 times.
  101. :param callable func: The function to call, you can pass arguments
  102. using :func:`functools.partial`:.
  103. :param odoo.api.Environment env: The environment where the registry
  104. and the cursor are taken.
  105. """
  106. try:
  107. for tryno in range(1, MAX_TRIES_ON_CONCURRENCY_FAILURE + 1):
  108. tryleft = MAX_TRIES_ON_CONCURRENCY_FAILURE - tryno
  109. try:
  110. result = func()
  111. if not env.cr._closed:
  112. env.cr.flush() # submit the changes to the database
  113. break
  114. except (IntegrityError, OperationalError) as exc:
  115. if env.cr._closed:
  116. raise
  117. env.cr.rollback()
  118. env.registry.reset_changes()
  119. if request:
  120. request.session = request._get_session_and_dbname()[0]
  121. # Rewind files in case of failure
  122. for filename, file in request.httprequest.files.items():
  123. if hasattr(file, "seekable") and file.seekable():
  124. file.seek(0)
  125. else:
  126. raise RuntimeError(f"Cannot retry request on input file {filename!r} after serialization failure") from exc
  127. if isinstance(exc, IntegrityError):
  128. raise _as_validation_error(env, exc) from exc
  129. if exc.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
  130. raise
  131. if not tryleft:
  132. _logger.info("%s, maximum number of tries reached!", errorcodes.lookup(exc.pgcode))
  133. raise
  134. wait_time = random.uniform(0.0, 2 ** tryno)
  135. _logger.info("%s, %s tries left, try again in %.04f sec...", errorcodes.lookup(exc.pgcode), tryleft, wait_time)
  136. time.sleep(wait_time)
  137. else:
  138. # handled in the "if not tryleft" case
  139. raise RuntimeError("unreachable")
  140. except Exception:
  141. env.registry.reset_changes()
  142. raise
  143. if not env.cr.closed:
  144. env.cr.commit() # effectively commits and execute post-commits
  145. env.registry.signal_changes()
  146. return result
  147. def _traverse_containers(val, type_):
  148. """ Yields atoms filtered by specified ``type_`` (or type tuple), traverses
  149. through standard containers (non-string mappings or sequences) *unless*
  150. they're selected by the type filter
  151. """
  152. from odoo.models import BaseModel
  153. if isinstance(val, type_):
  154. yield val
  155. elif isinstance(val, (str, bytes, BaseModel)):
  156. return
  157. elif isinstance(val, Mapping):
  158. for k, v in val.items():
  159. yield from _traverse_containers(k, type_)
  160. yield from _traverse_containers(v, type_)
  161. elif isinstance(val, Sequence):
  162. for v in val:
  163. yield from _traverse_containers(v, type_)