summaryrefslogtreecommitdiff
path: root/odoo
diff options
context:
space:
mode:
authorRaphael Collet <rco@odoo.com>2017-05-03 11:43:05 +0200
committerRaphael Collet <rco@odoo.com>2017-05-03 15:41:05 +0200
commitb59318ec12a1ce4e59de6d7bcde7146545b6c044 (patch)
treef1c23d20305c2dfae87e3f2ad27e6fa525c00f27 /odoo
parent1458f1b3135d373dac0625c7d4bd4a7eea3eaf54 (diff)
[REF] registry: always perform registry/cache signaling at the end of request
Problem: the update of custom models/fields is not fully transactional, and may potentially lead to an inconsistent database. An other problem is creating two custom fields by writing on a model: if the second one fails, the first one has been committed without notice. Retrying the request will give an unexpected error (duplicate field name). Solution: never commit in the middle of a request. If the changes have an impact on the registry, then mark it as invalid (with a new flag), and signal registry invalidation after everything has been committed. If the request fails, reset the registry. Both registry and cache invalidation are handled the same way.
Diffstat (limited to 'odoo')
-rw-r--r--odoo/addons/base/ir/ir_cron.py3
-rw-r--r--odoo/addons/base/ir/ir_model.py27
-rw-r--r--odoo/addons/base/res/res_font.py2
-rw-r--r--odoo/addons/base/tests/test_ir_actions.py17
-rw-r--r--odoo/http.py4
-rw-r--r--odoo/models.py6
-rw-r--r--odoo/modules/loading.py1
-rw-r--r--odoo/modules/registry.py73
-rw-r--r--odoo/service/model.py4
-rw-r--r--odoo/service/report.py4
-rw-r--r--odoo/tests/common.py1
-rw-r--r--odoo/tools/cache.py4
-rw-r--r--odoo/tools/sql.py1
13 files changed, 84 insertions, 63 deletions
diff --git a/odoo/addons/base/ir/ir_cron.py b/odoo/addons/base/ir/ir_cron.py
index 0c61f230813..5b0c6567ec7 100644
--- a/odoo/addons/base/ir/ir_cron.py
+++ b/odoo/addons/base/ir/ir_cron.py
@@ -95,8 +95,9 @@ class ir_cron(models.Model):
if start_time and _logger.isEnabledFor(logging.DEBUG):
end_time = time.time()
_logger.debug('%.3fs (cron %s, server action %d with uid %d)', end_time - start_time, cron_name, server_action_id, self.env.uid)
- self.pool.signal_caches_change()
+ self.pool.signal_changes()
except Exception as e:
+ self.pool.reset_changes()
self._handle_callback_exception(cron_name, server_action_id, job_id, e)
@classmethod
diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py
index f2d41c46847..49989a2c7a8 100644
--- a/odoo/addons/base/ir/ir_model.py
+++ b/odoo/addons/base/ir/ir_model.py
@@ -180,10 +180,8 @@ class IrModel(models.Model):
# Reload registry for normal unlink only. For module uninstall, the
# reload is done independently in odoo.modules.loading.
if not self._context.get(MODULE_UNINSTALL_FLAG):
- self._cr.commit() # must be committed before reloading registry in new cursor
- api.Environment.reset()
- registry = Registry.new(self._cr.dbname)
- registry.signal_registry_change()
+ # setup models; this automatically removes model from registry
+ self.pool.setup_models(self._cr)
return res
@@ -211,7 +209,6 @@ class IrModel(models.Model):
self.pool.setup_models(self._cr)
# update database schema
self.pool.init_models(self._cr, [vals['model']], dict(self._context, update_custom_fields=True))
- self.pool.signal_registry_change()
return res
@api.model
@@ -275,6 +272,11 @@ class IrModel(models.Model):
def _add_manual_models(self):
""" Add extra models to the registry. """
+ # clean up registry first
+ for name, model_class in self.pool.items():
+ if model_class._custom:
+ del self.pool.models[name]
+ # add manual models
cr = self.env.cr
cr.execute('SELECT * FROM ir_model WHERE state=%s', ['manual'])
for model_data in cr.dictfetchall():
@@ -591,12 +593,11 @@ class IrModelFields(models.Model):
# The field we just deleted might be inherited, and the registry is
# inconsistent in this case; therefore we reload the registry.
if not self._context.get(MODULE_UNINSTALL_FLAG):
- self._cr.commit()
- api.Environment.reset()
- registry = Registry.new(self._cr.dbname)
- models = registry.descendants(model_names, '_inherits')
- registry.init_models(self._cr, models, dict(self._context, update_custom_fields=True))
- registry.signal_registry_change()
+ # setup models; this re-initializes models in registry
+ self.pool.setup_models(self._cr)
+ # update database schema of model and its descendant models
+ models = self.pool.descendants(model_names, '_inherits')
+ self.pool.init_models(self._cr, models, dict(self._context, update_custom_fields=True))
return res
@@ -628,7 +629,6 @@ class IrModelFields(models.Model):
# update database schema of model and its descendant models
models = self.pool.descendants([vals['model']], '_inherits')
self.pool.init_models(self._cr, models, dict(self._context, update_custom_fields=True))
- self.pool.signal_registry_change()
return res
@@ -698,9 +698,6 @@ class IrModelFields(models.Model):
models = self.pool.descendants(patched_models, '_inherits')
self.pool.init_models(self._cr, models, dict(self._context, update_custom_fields=True))
- if column_rename or patched_models:
- self.pool.signal_registry_change()
-
return res
@api.multi
diff --git a/odoo/addons/base/res/res_font.py b/odoo/addons/base/res/res_font.py
index 873bea5aa03..ab3ad251c6c 100644
--- a/odoo/addons/base/res/res_font.py
+++ b/odoo/addons/base/res/res_font.py
@@ -91,7 +91,7 @@ class ResFont(models.Model):
# Remove inexistent fonts
self.search([('name', 'not in', existing_font_names), ('path', '!=', '/dev/null')]).unlink()
- self.pool.signal_caches_change()
+ self.pool.cache_invalidated = True
return self._sync()
def _sync(self):
diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py
index 344829c8181..4e0ee08d408 100644
--- a/odoo/addons/base/tests/test_ir_actions.py
+++ b/odoo/addons/base/tests/test_ir_actions.py
@@ -188,22 +188,19 @@ class TestCustomFields(common.TransactionCase):
MODEL = 'res.partner'
def setUp(self):
- # use a test cursor instead of a real cursor
+ # check that the registry is properly reset
registry = odoo.registry()
- registry.enter_test_mode()
fnames = set(registry[self.MODEL]._fields)
-
@self.addCleanup
- def callback():
- registry.leave_test_mode()
- # the tests may have modified the registry, reset it
- with registry.cursor() as cr:
- registry.clear_caches()
- registry.setup_models(cr)
- assert set(registry[self.MODEL]._fields) == fnames
+ def check_registry():
+ assert set(registry[self.MODEL]._fields) == fnames
super(TestCustomFields, self).setUp()
+ # use a test cursor instead of a real cursor
+ self.registry.enter_test_mode()
+ self.addCleanup(self.registry.leave_test_mode)
+
# do not reload the registry after removing a field
self.env = self.env(context={'_force_unlink': True})
diff --git a/odoo/http.py b/odoo/http.py
index 210361fce1b..0b3ad105e39 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -278,6 +278,9 @@ class WebRequest(object):
if self._cr:
if exc_type is None and not self._failed:
self._cr.commit()
+ self.registry.signal_changes()
+ else:
+ self.registry.reset_changes()
self._cr.close()
# just to be sure no one tries to re-use the request
self.disable_db = True
@@ -1467,7 +1470,6 @@ class Root(object):
result = _dispatch_nodb()
else:
result = ir_http._dispatch()
- ir_http.pool.signal_caches_change()
else:
result = _dispatch_nodb()
diff --git a/odoo/models.py b/odoo/models.py
index 10e962531f5..4f85dbf11ac 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -1541,11 +1541,7 @@ class BaseModel(object):
This clears the caches associated to methods decorated with
``tools.ormcache`` or ``tools.ormcache_multi``.
"""
- try:
- cls.pool.cache.clear()
- cls.pool.cache_cleared = True
- except AttributeError:
- pass
+ cls.pool._clear_cache()
@api.model
def _read_group_fill_results(self, domain, groupby, remaining_groupbys,
diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py
index 7536c344528..f8c65d40177 100644
--- a/odoo/modules/loading.py
+++ b/odoo/modules/loading.py
@@ -133,6 +133,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
registry.setup_models(cr)
registry.init_models(cr, model_names, {'module': package.name})
+ cr.commit()
idref = {}
diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py
index 9642219a88c..e0cfb532fd1 100644
--- a/odoo/modules/registry.py
+++ b/odoo/modules/registry.py
@@ -5,7 +5,7 @@
"""
from collections import Mapping, defaultdict, deque
-from contextlib import closing
+from contextlib import closing, contextmanager
from functools import partial
from operator import attrgetter
from weakref import WeakValueDictionary
@@ -98,10 +98,8 @@ class Registry(Mapping):
cr.commit()
registry.ready = True
+ registry.registry_invalidated = bool(update_module)
- if update_module:
- # only in case of update, otherwise we'll have an infinite reload loop!
- registry.signal_registry_change()
return registry
def init(self, db_name):
@@ -135,10 +133,9 @@ class Registry(Mapping):
self.registry_sequence = None
self.cache_sequence = None
- self.cache = LRU(8192)
- # Flag indicating if at least one model cache has been cleared.
- # Useful only in a multi-process context.
- self.cache_cleared = False
+ # Flags indicating invalidation of the registry or the cache.
+ self.registry_invalidated = False
+ self.cache_invalidated = False
with closing(self.cursor()) as cr:
has_unaccent = odoo.modules.db.has_unaccent(cr)
@@ -151,8 +148,9 @@ class Registry(Mapping):
""" Delete the registry linked to a given database. """
with cls._lock:
if db_name in cls.registries:
- cls.registries[db_name].clear_caches()
- del cls.registries[db_name]
+ registry = cls.registries.pop(db_name)
+ registry.clear_caches()
+ registry.registry_invalidated = True
@classmethod
def delete_all(cls):
@@ -278,6 +276,8 @@ class Registry(Mapping):
for model in models:
model._setup_complete()
+ self.registry_invalidated = True
+
def post_init(self, func, *args, **kwargs):
""" Register a function to call at the end of :meth:`~.init_models`. """
self._post_init_queue.append(partial(func, *args, **kwargs))
@@ -307,7 +307,6 @@ class Registry(Mapping):
if models:
models[0].recompute()
- cr.commit()
# make sure all tables are present
missing = [name
@@ -321,17 +320,26 @@ class Registry(Mapping):
if name in missing:
_logger.info("Recreate table of model %s.", name)
env[name].init()
- cr.commit()
# check again, and log errors if tables are still missing
for name, model in env.items():
if not model._abstract and not table_exists(cr, model._table):
_logger.error("Model %s has no table.", name)
+ @lazy_property
+ def cache(self):
+ """ A cache for model methods. """
+ # this lazy_property is automatically reset by lazy_property.reset_all()
+ return LRU(8192)
+
+ def _clear_cache(self):
+ """ Clear the cache and mark it as invalidated. """
+ self.cache.clear()
+ self.cache_invalidated = True
+
def clear_caches(self):
""" Clear the caches associated to methods decorated with
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models.
"""
- self.cache.clear()
for model in self.models.itervalues():
model.clear_caches()
@@ -381,29 +389,50 @@ class Registry(Mapping):
elif self.cache_sequence != c:
_logger.info("Invalidating all model caches after database signaling.")
self.clear_caches()
- self.cache_cleared = False
+ self.cache_invalidated = False
self.registry_sequence = r
self.cache_sequence = c
return self
- def signal_registry_change(self):
- """ Notifies other processes that the registry has changed. """
- if odoo.multi_process:
+ def signal_changes(self):
+ """ Notifies other processes if registry or cache has been invalidated. """
+ if odoo.multi_process and self.registry_invalidated:
_logger.info("Registry changed, signaling through the database")
with closing(self.cursor()) as cr:
cr.execute("select nextval('base_registry_signaling')")
self.registry_sequence = cr.fetchone()[0]
- def signal_caches_change(self):
- """ Notifies other processes if caches have been invalidated. """
- if odoo.multi_process and self.cache_cleared:
- # signal it through the database to other processes
+ # no need to notify cache invalidation in case of registry invalidation,
+ # because reloading the registry implies starting with an empty cache
+ elif odoo.multi_process and self.cache_invalidated:
_logger.info("At least one model cache has been invalidated, signaling through the database.")
with closing(self.cursor()) as cr:
cr.execute("select nextval('base_cache_signaling')")
self.cache_sequence = cr.fetchone()[0]
- self.cache_cleared = False
+
+ self.registry_invalidated = False
+ self.cache_invalidated = False
+
+ def reset_changes(self):
+ """ Reset the registry and cancel all invalidations. """
+ if self.registry_invalidated:
+ with closing(self.cursor()) as cr:
+ self.setup_models(cr)
+ self.registry_invalidated = False
+ if self.cache_invalidated:
+ self.cache.clear()
+ self.cache_invalidated = False
+
+ @contextmanager
+ def manage_changes(self):
+ """ Context manager to signal/discard registry and cache invalidations. """
+ try:
+ yield self
+ self.signal_changes()
+ except Exception:
+ self.reset_changes()
+ raise
def in_test_mode(self):
""" Test whether the registry is in 'test' mode. """
diff --git a/odoo/service/model.py b/odoo/service/model.py
index 9ceab82b452..ba243bf432c 100644
--- a/odoo/service/model.py
+++ b/odoo/service/model.py
@@ -36,8 +36,8 @@ def dispatch(method, params):
security.check(db,uid,passwd)
registry = odoo.registry(db).check_signaling()
fn = globals()[method]
- res = fn(db, uid, *params)
- registry.signal_caches_change()
+ with registry.manage_changes():
+ res = fn(db, uid, *params)
return res
def check(f):
diff --git a/odoo/service/report.py b/odoo/service/report.py
index faac5521b99..8229a2396e4 100644
--- a/odoo/service/report.py
+++ b/odoo/service/report.py
@@ -32,8 +32,8 @@ def dispatch(method, params):
security.check(db,uid,passwd)
registry = odoo.registry(db).check_signaling()
fn = globals()['exp_' + method]
- res = fn(db, uid, *params)
- registry.signal_caches_change()
+ with registry.manage_changes():
+ res = fn(db, uid, *params)
return res
def exp_render_report(db, uid, object, ids, datas=None, context=None):
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index ce7be91ffd4..78cfbfbdbf9 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -156,6 +156,7 @@ class TransactionCase(BaseCase):
def reset():
# rollback and close the cursor, and reset the environments
self.registry.clear_caches()
+ self.registry.reset_changes()
self.env.reset()
self.cr.rollback()
self.cr.close()
diff --git a/odoo/tools/cache.py b/odoo/tools/cache.py
index 3703fc83ab1..48dc42ba04c 100644
--- a/odoo/tools/cache.py
+++ b/odoo/tools/cache.py
@@ -92,9 +92,7 @@ class ormcache(object):
def clear(self, model, *args):
""" Clear the registry cache """
- d, key0, _ = self.lru(model)
- d.clear()
- model.pool.cache_cleared = True
+ model.pool._clear_cache()
class ormcache_context(ormcache):
diff --git a/odoo/tools/sql.py b/odoo/tools/sql.py
index c4e9a143a8e..563ed08c679 100644
--- a/odoo/tools/sql.py
+++ b/odoo/tools/sql.py
@@ -189,7 +189,6 @@ def drop_index(cr, indexname, tablename):
def drop_view_if_exists(cr, viewname):
cr.execute("DROP view IF EXISTS %s CASCADE" % (viewname,))
- cr.commit()
def escape_psql(to_escape):
return to_escape.replace('\\', r'\\').replace('%', '\%').replace('_', '\_')