diff --git a/odoo/addons/base/tests/test_ormcache.py b/odoo/addons/base/tests/test_ormcache.py index e92281ce62f..7de21986665 100644 --- a/odoo/addons/base/tests/test_ormcache.py +++ b/odoo/addons/base/tests/test_ormcache.py @@ -97,3 +97,78 @@ class TestOrmcache(TransactionCase): ['reset_changes'] * nb_treads + ['assert_empty'] * nb_treads ) + + def test_signaling_01_single(self): + self.assertFalse(self.registry.test_cr) + self.registry.cache_invalidated.clear() + registry = self.registry + old_sequences = dict(registry.cache_sequences) + with self.assertLogs('odoo.modules.registry') as logs: + registry.cache_invalidated.add('assets') + self.assertEqual(registry.cache_invalidated, {'assets'}) + registry.signal_changes() + self.assertFalse(registry.cache_invalidated) + + self.assertEqual( + logs.output, + ["INFO:odoo.modules.registry:Caches invalidated, signaling through the database: ['assets']"], + ) + + for key, value in old_sequences.items(): + if key == 'assets': + self.assertEqual(value + 1, registry.cache_sequences[key], "Assets cache sequence should have changed") + else: + self.assertEqual(value, registry.cache_sequences[key], "other registry sequence shouldn't have changed") + + with self.assertNoLogs(None, None): # the registry sequence should be up to date on the same worker + registry.check_signaling() + + # simulate other worker state + + registry.cache_sequences.update(old_sequences) + + with self.assertLogs() as logs: + registry.check_signaling() + self.assertEqual( + logs.output, + ["INFO:odoo.modules.registry:Invalidating caches after database signaling: ['assets', 'templates.cached_values']"], + ) + + def test_signaling_01_multiple(self): + self.assertFalse(self.registry.test_cr) + self.registry.cache_invalidated.clear() + registry = self.registry + old_sequences = dict(registry.cache_sequences) + with self.assertLogs('odoo.modules.registry') as logs: + registry.cache_invalidated.add('assets') + registry.cache_invalidated.add('default') + self.assertEqual(registry.cache_invalidated, {'assets', 'default'}) + registry.signal_changes() + self.assertFalse(registry.cache_invalidated) + + self.assertEqual( + logs.output, + [ + "INFO:odoo.modules.registry:Caches invalidated, signaling through the database: ['assets', 'default']", + ], + ) + + for key, value in old_sequences.items(): + if key in ('assets', 'default'): + self.assertEqual(value + 1, registry.cache_sequences[key], "Assets and default cache sequence should have changed") + else: + self.assertEqual(value, registry.cache_sequences[key], "other registry sequence shouldn't have changed") + + with self.assertNoLogs(None, None): # the registry sequence should be up to date on the same worker + registry.check_signaling() + + # simulate other worker state + + registry.cache_sequences.update(old_sequences) + + with self.assertLogs() as logs: + registry.check_signaling() + self.assertEqual( + logs.output, + ["INFO:odoo.modules.registry:Invalidating caches after database signaling: ['assets', 'default', 'templates.cached_values']"], + ) diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index ac8cf973608..4b048c17074 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -40,11 +40,19 @@ _REGISTRY_CACHES = { 'default': 8192, 'assets': 512, # arbitrary 'templates': 1024, # arbitrary - 'templates.cached_values': 2048, # arbitrary 'routing': 1024, # 2 entries per website 'routing.rewrites': 8192, # url_rewrite entries + 'templates.cached_values': 2048, # arbitrary } +# cache invalidation dependencies, as follows: +# { 'cache_key': ('cache_container_1', 'cache_container_3', ...) } +_CACHES_BY_KEY = { + 'default': ('default', 'templates.cached_values'), + 'assets': ('assets', 'templates.cached_values'), + 'templates': ('templates', 'templates.cached_values'), + 'routing': ('routing', 'routing.rewrites', 'templates.cached_values'), +} class Registry(Mapping): """ Model registry for a particular database. @@ -134,9 +142,6 @@ class Registry(Mapping): self._ordinary_tables = None self._constraint_queue = deque() self.__caches = {cache_name: LRU(cache_size) for cache_name, cache_size in _REGISTRY_CACHES.items()} - self.__caches_by_prefix = {} - for cache_name, cache in self.__caches.items(): - self.__caches_by_prefix.setdefault(cache_name.split('.')[0], []).append(cache) # modules fully loaded (maintained during init phase by `loading` module) self._init_modules = set() @@ -721,8 +726,8 @@ class Registry(Mapping): cache_names = cache_names or ('default',) assert not any('.' in cache_name for cache_name in cache_names) for cache_name in cache_names: - for cache in self.__caches_by_prefix[cache_name]: - cache.clear() + for cache in _CACHES_BY_KEY[cache_name]: + self.__caches[cache].clear() self.cache_invalidated.add(cache_name) # log information about invalidation_cause @@ -736,9 +741,9 @@ class Registry(Mapping): """ Clear the caches associated to methods decorated with ``tools.ormcache``. """ - for cache_name, caches in self.__caches_by_prefix.items(): + for cache_name, caches in _CACHES_BY_KEY.items(): for cache in caches: - cache.clear() + self.__caches[cache].clear() self.cache_invalidated.add(cache_name) caller_info = format_frame(inspect.currentframe().f_back) @@ -791,7 +796,7 @@ class Registry(Mapping): # must be reloaded. # The `base_cache_signaling_...` sequences indicates when caches must # be invalidated (i.e. cleared). - sequence_names = ('base_registry_signaling', *(f'base_cache_signaling_{cache_name}' for cache_name in self.__caches_by_prefix)) + sequence_names = ('base_registry_signaling', *(f'base_cache_signaling_{cache_name}' for cache_name in _CACHES_BY_KEY)) cr.execute("SELECT sequence_name FROM information_schema.sequences WHERE sequence_name IN %s", [sequence_names]) existing_sequences = tuple(s[0] for s in cr.fetchall()) # could be a set but not efficient with such a little list @@ -811,14 +816,14 @@ class Registry(Mapping): self.registry_sequence, ' '.join('[Cache %s: %s]' % cs for cs in self.cache_sequences.items())) def get_sequences(self, cr): - cache_sequences_query = ', '.join([f'base_cache_signaling_{cache_name}' for cache_name in self.__caches_by_prefix]) - cache_sequences_values_query = ',\n'.join([f'base_cache_signaling_{cache_name}.last_value' for cache_name in self.__caches_by_prefix]) + cache_sequences_query = ', '.join([f'base_cache_signaling_{cache_name}' for cache_name in _CACHES_BY_KEY]) + cache_sequences_values_query = ',\n'.join([f'base_cache_signaling_{cache_name}.last_value' for cache_name in _CACHES_BY_KEY]) cr.execute(f""" SELECT base_registry_signaling.last_value, {cache_sequences_values_query} FROM base_registry_signaling, {cache_sequences_query} """) registry_sequence, *cache_sequences_values = cr.fetchone() - cache_sequences = dict(zip(self.__caches_by_prefix, cache_sequences_values)) + cache_sequences = dict(zip(_CACHES_BY_KEY, cache_sequences_values)) return registry_sequence, cache_sequences def check_signaling(self): @@ -840,15 +845,19 @@ class Registry(Mapping): changes += "[Registry - %s -> %s]" % (self.registry_sequence, db_registry_sequence) # Check if the model caches must be invalidated. else: + invalidated = [] for cache_name, cache_sequence in self.cache_sequences.items(): expected_sequence = db_cache_sequences[cache_name] if cache_sequence != expected_sequence: - _logger.info("Invalidating %s model caches after database signaling.", cache_name) - for cache in self.__caches_by_prefix[cache_name]: # don't call clear_cache to avoid signal loop - cache.clear() + for cache in _CACHES_BY_KEY[cache_name]: # don't call clear_cache to avoid signal loop + if cache not in invalidated: + invalidated.append(cache) + self.__caches[cache].clear() self.cache_sequences[cache_name] = expected_sequence if _logger.isEnabledFor(logging.DEBUG): changes += "[Cache %s - %s -> %s]" % (cache_name, cache_sequence, expected_sequence) + if invalidated: + _logger.info("Invalidating caches after database signaling: %s", sorted(invalidated)) if changes: _logger.debug("Multiprocess signaling check: %s", changes) return self @@ -873,7 +882,7 @@ class Registry(Mapping): # no need to notify cache invalidation in case of registry invalidation, # because reloading the registry implies starting with an empty cache elif self.cache_invalidated: - _logger.info("At least one model cache has been invalidated, signaling through the database.") + _logger.info("Caches invalidated, signaling through the database: %s", sorted(self.cache_invalidated)) with closing(self.cursor()) as cr: for cache_name in self.cache_invalidated: cr.execute("select nextval(%s)", [f'base_cache_signaling_{cache_name}']) @@ -890,8 +899,8 @@ class Registry(Mapping): self.registry_invalidated = False if self.cache_invalidated: for cache_name in self.cache_invalidated: - for cache in self.__caches_by_prefix[cache_name]: - cache.clear() + for cache in _CACHES_BY_KEY[cache_name]: + self.__caches[cache].clear() self.cache_invalidated.clear() @contextmanager