Files
odoo_source/odoo/service/model.py
T
Julien Castiaux 04e972660b [IMP] core: don't save visitor default session
Every request comes with a session, a dictionary that is persisted on
the filesystem and that saves various information such as the user
cart on the ecommerce.

When a user simply visits the website, a default session is created and
saved on disk, this bloats the filestore with many sessions. Creating
the session on-the-fly is cheaper than loading it from the filesystem.
With this work the default session is not saved on disk anymore unless
explicitly asked via `session.touch()`.

An exception to the statement "creating the session on-the-fly is
cheaper" is geoip, the ip geolocalization is not cheap. In this work,
geoip have been moved from http_routing/request.session.geoip to a
lazy property core/request.geoip. When requested the info is persisted
on the session. Like other keys from the default session, geoip will not
be persisted unless there is non-default stuff in the session.

Because the CSRF-TOKEN is based on the session-id, it is important the
session-id stays the same across multiples requests even when the
session is not persisted on disk. Even when a session is not persisted
on disk, the session-id cookie is still set so that the next session
created on-the-fly uses the same session-id.

Technical note regarding the session, it has been decided to drop the
session-snapshot protocol and to reintroduce a "modified" flag. It has
been decided not to use werkzeug's session (which natively comes with a
"modified" flag) and to keep our own session object. We decided to
extend MutableMapping instead of dict; using MutableMapping we only
have to override __setitem__ and __detitem__; using dict we would had to
override update()/pop()/... too.

Task: 2789035
Part-of: odoo/odoo#86015
2022-04-05 14:13:54 +02:00

170 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 = request._get_session_and_dbname()[0]
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