diff --git a/base_registry_cache_custom/README.rst b/base_registry_cache_custom/README.rst new file mode 100644 index 00000000000..23e7c6f2b09 --- /dev/null +++ b/base_registry_cache_custom/README.rst @@ -0,0 +1,131 @@ +===================== +Registry Custom Cache +===================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6c8e306992a72a18f30d78f12456cec5b1802ae1fde1c9179f122297a61c4562 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/17.0/base_registry_cache_custom + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-17-0/server-tools-17-0-base_registry_cache_custom + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows adding custom caches to the DBs' registries. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Make sure you run Odoo with +``--load=base,web,base_registry_cache_custom`` or using the Odoo +configuration file: + +.. code:: ini + + [options] + (...) + server_wide_modules = base,web,base_registry_cache_custom + +Usage +===== + +If you need to create a custom cache, create a new module and: + +- add this module as its dependency +- add a ``post_load`` hook like this: + +.. code:: python + + from odoo.addons.base_registry_cache_custom.registry import add_custom_cache + + + def post_load(): + add_custom_cache(name="my_cache", count=256) + +If you make use of multiple caches, and some of them should be +invalidated when another one gets invalidated itself, use the +``depends_on_caches`` argument: + +.. code:: python + + from odoo.addons.base_registry_cache_custom.registry import add_custom_cache + + + def post_load(): + add_custom_cache(name="my_cache_1", count=256) + add_custom_cache(name="my_cache_2", count=128, depends_on_caches=["my_cache"]) + +You can also add sub-caches, which can be declared using dotted names, +and will be invalidated only when one of their dependency caches are +invalidated: + +.. code:: python + + from odoo.addons.base_registry_cache_custom.registry import add_custom_cache + + + def post_load(): + add_custom_cache(name="my_cache", count=256) + add_custom_cache(name="my_cache.subcache", count=128, depends_on_caches=["my_cache"], allows_direct_invalidation=False) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- Silvio Gregorini + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_registry_cache_custom/__init__.py b/base_registry_cache_custom/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/base_registry_cache_custom/__manifest__.py b/base_registry_cache_custom/__manifest__.py new file mode 100644 index 00000000000..6c891e689a1 --- /dev/null +++ b/base_registry_cache_custom/__manifest__.py @@ -0,0 +1,14 @@ +# Copyright 2025 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +{ + "name": "Registry Custom Cache", + "version": "17.0.1.0.0", + "category": "Tools", + "summary": "Add custom caches to Odoo registries", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "LGPL-3", + "website": "https://github.com/OCA/server-tools", + "depends": ["base"], + "installable": True, +} diff --git a/base_registry_cache_custom/exceptions.py b/base_registry_cache_custom/exceptions.py new file mode 100644 index 00000000000..19580a6485a --- /dev/null +++ b/base_registry_cache_custom/exceptions.py @@ -0,0 +1,22 @@ +# Copyright 2025 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + + +class CacheError(Exception): + """Base error for invalid cache configuration""" + + +class CacheAlreadyExistsError(CacheError): + """Cache cannot be added to the registry because it already exists""" + + +class CacheInvalidConfigError(CacheError): + """Invalid cache configuration""" + + +class CacheInvalidDependencyError(CacheInvalidConfigError): + """Invalid cache dependency""" + + +class CacheInvalidNameError(CacheInvalidConfigError): + """Invalid cache name""" diff --git a/base_registry_cache_custom/pyproject.toml b/base_registry_cache_custom/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/base_registry_cache_custom/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_registry_cache_custom/readme/CONFIGURE.md b/base_registry_cache_custom/readme/CONFIGURE.md new file mode 100644 index 00000000000..02467202d0d --- /dev/null +++ b/base_registry_cache_custom/readme/CONFIGURE.md @@ -0,0 +1,7 @@ +Make sure you run Odoo with ``--load=base,web,base_registry_cache_custom`` or using the Odoo configuration file: + +``` ini +[options] +(...) +server_wide_modules = base,web,base_registry_cache_custom +``` diff --git a/base_registry_cache_custom/readme/CONTRIBUTORS.md b/base_registry_cache_custom/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..82b9a91c10e --- /dev/null +++ b/base_registry_cache_custom/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Silvio Gregorini \<\> diff --git a/base_registry_cache_custom/readme/DESCRIPTION.md b/base_registry_cache_custom/readme/DESCRIPTION.md new file mode 100644 index 00000000000..6618b243f13 --- /dev/null +++ b/base_registry_cache_custom/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows adding custom caches to the DBs' registries. diff --git a/base_registry_cache_custom/readme/USAGE.md b/base_registry_cache_custom/readme/USAGE.md new file mode 100644 index 00000000000..b493ea6f454 --- /dev/null +++ b/base_registry_cache_custom/readme/USAGE.md @@ -0,0 +1,36 @@ +If you need to create a custom cache, create a new module and: + +- add this module as its dependency +- add a `post_load` hook like this: + +```python +from odoo.addons.base_registry_cache_custom.registry import add_custom_cache + + +def post_load(): + add_custom_cache(name="my_cache", count=256) +``` + +If you make use of multiple caches, and some of them should be invalidated when another +one gets invalidated itself, use the `depends_on_caches` argument: + +```python +from odoo.addons.base_registry_cache_custom.registry import add_custom_cache + + +def post_load(): + add_custom_cache(name="my_cache_1", count=256) + add_custom_cache(name="my_cache_2", count=128, depends_on_caches=["my_cache"]) +``` + +You can also add sub-caches, which can be declared using dotted names, and will be +invalidated only when one of their dependency caches are invalidated: + +```python +from odoo.addons.base_registry_cache_custom.registry import add_custom_cache + + +def post_load(): + add_custom_cache(name="my_cache", count=256) + add_custom_cache(name="my_cache.subcache", count=128, depends_on_caches=["my_cache"], allows_direct_invalidation=False) +``` diff --git a/base_registry_cache_custom/registry.py b/base_registry_cache_custom/registry.py new file mode 100644 index 00000000000..3169fd444a9 --- /dev/null +++ b/base_registry_cache_custom/registry.py @@ -0,0 +1,122 @@ +# Copyright 2025 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +import logging +import typing + +from odoo.modules import registry +from odoo.tools.lru import LRU +from odoo.tools.misc import OrderedSet + +from .exceptions import ( + CacheAlreadyExistsError, + CacheInvalidConfigError, + CacheInvalidDependencyError, + CacheInvalidNameError, +) + +_logger = logging.getLogger(__name__) + + +def add_custom_cache( + name: str, + count: int, + depends_on_caches: typing.Iterable[str] = None, + allows_direct_invalidation: bool = True, + ignore_exceptions: typing.Iterable[type] = (), +): + """Adds a custom cache into the Odoo registry + + :param name: name of the custom cache to add + :param count: max capability of the custom cache; set as 1 if a lower value is given + :param allows_direct_invalidation: if ``True``, a DB sequence is assigned to the + cache is assigned any sequence and (therefore dotted names for such caches are + not allowed) and method ``Registry.clear_cache()`` can be called directly for it + :param depends_on_caches: iterable of other cache names: if set, the current cache + will be listed as dependent on those caches, and invalidating one of them will + invalidate the custom cache as well + :param ignore_exceptions: iterable of Exception types: if set, no error is raised, + but the cache is not added to the registries anyway + """ + # Backup ``registry`` module attributes that needs restoring if something goes wrong + registry_caches_backup = dict(registry._REGISTRY_CACHES) + caches_by_key_backup = dict(registry._CACHES_BY_KEY) + try: + _add_custom_cache(name, count, depends_on_caches, allows_direct_invalidation) + except Exception as exc: + _logger.error(f"Could not add custom cache '{name}': {exc}") + # Restore ``registry`` module attributes + registry._REGISTRY_CACHES = registry_caches_backup + registry._CACHES_BY_KEY = caches_by_key_backup + # Raise error unless specified otherwise + ignore_exceptions = tuple(ignore_exceptions or ()) + if not (ignore_exceptions and isinstance(exc, ignore_exceptions)): + raise + + +def _add_custom_cache( + name: str, + count: int, + depends_on_caches: typing.Iterable[str] = None, + allows_direct_invalidation: bool = True, +): + _logger.info(f"Adding cache '{name}' to registries...") + + # ``registry._REGISTRY_CACHES`` is used by ``registry.Registry.init()`` to + # initialize registries' caches (attr ``__cache``) + if name in registry._REGISTRY_CACHES: + raise CacheAlreadyExistsError(f"Cache '{name}' already exists") + normalized_count = max(count, 1) + registry._REGISTRY_CACHES[name] = normalized_count + + # ``registry._CACHES_BY_KEY`` is used by a variety of ``registry.Registry`` + # methods to handle caches dependencies and DB signaling (main reason why a + # cache that allows direct invalidation cannot have a dotted name) + if allows_direct_invalidation: + if "." in name: + raise CacheInvalidNameError(f"Invalid cache name '{name}'") + registry._CACHES_BY_KEY[name] = (name,) + elif not depends_on_caches: + raise CacheInvalidConfigError( + f"Cache '{name}' should either allow direct invalidation" + f" or depend on another cache for indirect invalidation" + ) + + # Setup invalidation dependencies recursively: if cache-3 depends on cache-2, + # and cache-2 depends on cache-1, then invalidating cache-1 should invalidate + # cache-3 too + # NB: use an ``OrderedSet`` to avoid duplicates while keeping the dependency + # order, then convert to tuple for consistency w/ the standard + # ``registry._CACHES_BY_KEY`` structure + if depends_on_caches: + for cache in depends_on_caches or []: + if cache not in registry._CACHES_BY_KEY: + raise CacheInvalidDependencyError( + f"Cache '{name}' cannot depend on cache '{cache}':" + f" '{cache}' doesn't exist or doesn't allow direct invalidation" + ) + deps = OrderedSet(registry._CACHES_BY_KEY[cache]) + registry._CACHES_BY_KEY[cache] = tuple(deps | {name}) + to_check = list(registry._CACHES_BY_KEY) + while to_check: + cache = to_check.pop(0) + deps = OrderedSet(registry._CACHES_BY_KEY[cache]) + for dep in tuple(deps): + for subdep in registry._CACHES_BY_KEY.get(dep) or []: + if subdep not in deps: + deps.add(subdep) + if cache not in to_check: + to_check.append(cache) + registry._CACHES_BY_KEY[cache] = tuple(OrderedSet([cache]) | deps) + + # Update existing registries by: + # - adding the custom cache to the registry (name-mangle: no AttributeError) + # - setting up the proper signaling workflow + # NB: ``registry.Registry.registries`` is a class attribute that returns an + # ``odoo.tools.lru.LRU`` object that maps DB names to ``registry.Registry`` + # objects through variable ``d`` (which is a ``collections.OrderedDict`` object) + for db_name, db_registry in registry.Registry.registries.d.items(): + _logger.info(f"Adding cache '{name}' to '{db_name}' registry") + db_registry._Registry__caches[name] = LRU(normalized_count) + if allows_direct_invalidation: + db_registry.setup_signaling() diff --git a/base_registry_cache_custom/static/description/index.html b/base_registry_cache_custom/static/description/index.html new file mode 100644 index 00000000000..bb487a0653b --- /dev/null +++ b/base_registry_cache_custom/static/description/index.html @@ -0,0 +1,473 @@ + + + + + +Registry Custom Cache + + + +
+

Registry Custom Cache

+ + +

Beta License: LGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

This module allows adding custom caches to the DBs’ registries.

+

Table of contents

+ +
+

Configuration

+

Make sure you run Odoo with +--load=base,web,base_registry_cache_custom or using the Odoo +configuration file:

+
+[options]
+(...)
+server_wide_modules = base,web,base_registry_cache_custom
+
+
+
+

Usage

+

If you need to create a custom cache, create a new module and:

+
    +
  • add this module as its dependency
  • +
  • add a post_load hook like this:
  • +
+
+from odoo.addons.base_registry_cache_custom.registry import add_custom_cache
+
+
+def post_load():
+    add_custom_cache(name="my_cache", count=256)
+
+

If you make use of multiple caches, and some of them should be +invalidated when another one gets invalidated itself, use the +depends_on_caches argument:

+
+from odoo.addons.base_registry_cache_custom.registry import add_custom_cache
+
+
+def post_load():
+    add_custom_cache(name="my_cache_1", count=256)
+    add_custom_cache(name="my_cache_2", count=128, depends_on_caches=["my_cache"])
+
+

You can also add sub-caches, which can be declared using dotted names, +and will be invalidated only when one of their dependency caches are +invalidated:

+
+from odoo.addons.base_registry_cache_custom.registry import add_custom_cache
+
+
+def post_load():
+    add_custom_cache(name="my_cache", count=256)
+    add_custom_cache(name="my_cache.subcache", count=128, depends_on_caches=["my_cache"], allows_direct_invalidation=False)
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/server-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/base_registry_cache_custom/tests/__init__.py b/base_registry_cache_custom/tests/__init__.py new file mode 100644 index 00000000000..8000b54ca1d --- /dev/null +++ b/base_registry_cache_custom/tests/__init__.py @@ -0,0 +1 @@ +from . import test_registry_cache_custom diff --git a/base_registry_cache_custom/tests/test_registry_cache_custom.py b/base_registry_cache_custom/tests/test_registry_cache_custom.py new file mode 100644 index 00000000000..7b05c0df182 --- /dev/null +++ b/base_registry_cache_custom/tests/test_registry_cache_custom.py @@ -0,0 +1,291 @@ +# Copyright 2025 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo_test_helper import FakeModelLoader + +from odoo import models +from odoo.modules import registry +from odoo.tests.common import TransactionCase +from odoo.tools.cache import get_cache_key_counter, ormcache +from odoo.tools.misc import mute_logger + +from odoo.addons.base_registry_cache_custom.exceptions import ( + CacheAlreadyExistsError, + CacheInvalidConfigError, + CacheInvalidDependencyError, + CacheInvalidNameError, +) +from odoo.addons.base_registry_cache_custom.registry import add_custom_cache + + +class TestRegistryCacheCustom(TransactionCase): + def setUp(self): + super().setUp() + # Prepare a backup of the ``registry`` module attributes to be able to restore + # them once each test is over to avoid polluting other tests + self.__REGISTRY_CACHES_BACKUP = dict(registry._REGISTRY_CACHES) + self.__CACHES_BY_KEY_BACKUP = dict(registry._CACHES_BY_KEY) + + def tearDown(self): + # Restore ``registry`` module attributes + registry._REGISTRY_CACHES = self.__REGISTRY_CACHES_BACKUP + registry._CACHES_BY_KEY = self.__CACHES_BY_KEY_BACKUP + super().tearDown() + + @mute_logger("odoo.addons.base_registry_cache_custom.registry") + def test_01_add_custom_cache(self): + """Base checks for adding custom caches + + Checks that ``add_custom_cache()`` adds the new cache to all DBs' registries, + and that the signaling setup is correctly done for each DB + """ + add_custom_cache("test_cache", 10) + for db_registry in type(self.env.registry).registries.d.values(): + self.assertIn("test_cache", db_registry._Registry__caches) + self.assertEqual(db_registry._Registry__caches["test_cache"].count, 10) + with db_registry.cursor() as cr: + cr.execute( + """ + SELECT sequence_name + FROM information_schema.sequences + WHERE sequence_name = 'base_cache_signaling_test_cache' + """ + ) + self.assertEqual(cr.fetchall(), [("base_cache_signaling_test_cache",)]) + self.assertIn("test_cache", db_registry.get_sequences(cr)[1]) + + @mute_logger("odoo.addons.base_registry_cache_custom.registry") + def test_02_add_custom_cache_count(self): + """Checks that using ``count <= 0`` creates caches w/ ``count = 1``""" + for name, count in (("test_cache_zero", 0), ("test_cache_negative", -1)): + add_custom_cache(name, count) + for db_registry in type(self.env.registry).registries.d.values(): + self.assertEqual(db_registry._Registry__caches[name].count, 1) + + @mute_logger("odoo.addons.base_registry_cache_custom.registry") + def test_03_add_custom_cache_existing(self): + """Checks a ``ValueError`` is raised when trying to add an existing cache""" + add_custom_cache("test_cache", 10) + with self.assertRaisesRegex( + CacheAlreadyExistsError, r"^Cache 'test_cache' already exists$" + ): + add_custom_cache("test_cache", 10) + + @mute_logger("odoo.addons.base_registry_cache_custom.registry") + def test_04_add_custom_cache_dotted_name_invalid(self): + """Checks a ``ValueError`` is raised when trying to use a dotted name""" + with self.assertRaisesRegex( + CacheInvalidNameError, r"^Invalid cache name 'test\.cache'$" + ): + add_custom_cache("test.cache", 10) + + @mute_logger("odoo.addons.base_registry_cache_custom.registry") + def test_05_add_custom_cache_dependent(self): + """Checks creation of caches whose invalidation depend on another cache""" + add_custom_cache("test_cache_1", 10) + add_custom_cache("test_cache_2", 10, ["test_cache_1"]) + add_custom_cache("test_cache_3", 10, ["test_cache_1"]) + add_custom_cache("test_cache_4", 10, ["test_cache_2", "test_cache_3"]) + db_registry = self.env.registry + + def _fill_caches(): + for i in range(1, 5): + db_registry._Registry__caches[f"test_cache_{i}"]["key"] = 1 + + # Put some data in the caches, then invalidate the parent cache and check that + # the cache itself and all dependent caches are empty + _fill_caches() + db_registry.clear_cache("test_cache_1") + self.assertFalse(db_registry._Registry__caches["test_cache_1"]) + self.assertFalse(db_registry._Registry__caches["test_cache_2"]) + self.assertFalse(db_registry._Registry__caches["test_cache_3"]) + self.assertFalse(db_registry._Registry__caches["test_cache_4"]) + + _fill_caches() + db_registry.clear_cache("test_cache_2") + self.assertEqual(db_registry._Registry__caches["test_cache_1"]["key"], 1) + self.assertFalse(db_registry._Registry__caches["test_cache_2"]) + self.assertEqual(db_registry._Registry__caches["test_cache_3"]["key"], 1) + self.assertFalse(db_registry._Registry__caches["test_cache_4"]) + + _fill_caches() + db_registry.clear_cache("test_cache_3") + self.assertEqual(db_registry._Registry__caches["test_cache_1"]["key"], 1) + self.assertEqual(db_registry._Registry__caches["test_cache_2"]["key"], 1) + self.assertFalse(db_registry._Registry__caches["test_cache_3"]) + self.assertFalse(db_registry._Registry__caches["test_cache_4"]) + + _fill_caches() + db_registry.clear_cache("test_cache_4") + self.assertEqual(db_registry._Registry__caches["test_cache_1"]["key"], 1) + self.assertEqual(db_registry._Registry__caches["test_cache_2"]["key"], 1) + self.assertEqual(db_registry._Registry__caches["test_cache_3"]["key"], 1) + self.assertFalse(db_registry._Registry__caches["test_cache_4"]) + + @mute_logger("odoo.addons.base_registry_cache_custom.registry") + def test_06_add_custom_cache_no_direct_invalidation(self): + """Checks creation of caches that cannot be directly invalidated""" + add_custom_cache("test_cache_1", 10) + add_custom_cache( + # Cannot be directly invalidated => no DB sequence => can use dotted name + "test.cache.2", + 10, + depends_on_caches=["test_cache_1"], + allows_direct_invalidation=False, + ) + add_custom_cache( + "test_cache_3", + 10, + depends_on_caches=["test_cache_1"], + allows_direct_invalidation=False, + ) + + # ``Registry.clear_cache()`` first checks whether the cache name is not dotted + with self.assertRaises(AssertionError): + self.env.registry.clear_cache("test.cache.2") + + # ``Registry.clear_cache()`` then access ``_CACHE_BY_KEY`` to get dependent + # caches to invalidate, but "test_cache_3" does not allow direct invalidation, + # so it's not included in that mapping + with self.assertRaises(KeyError): + self.env.registry.clear_cache("test_cache_3") + + # A cache that doesn't allow direct invalidation requires a parent cache + with self.assertRaisesRegex( + CacheInvalidConfigError, + r"^Cache 'test_cache_4' should either allow direct invalidation" + " or depend on another cache for indirect invalidation$", + ): + add_custom_cache("test_cache_4", 10, allows_direct_invalidation=False) + + # Parent caches must exist + with self.assertRaisesRegex( + CacheInvalidDependencyError, + r"^Cache 'test_cache_5' cannot depend on cache 'test_cache_6':" + " 'test_cache_6' doesn't exist or doesn't allow direct invalidation$", + ): + add_custom_cache( + "test_cache_5", + 10, + depends_on_caches=["test_cache_6"], + allows_direct_invalidation=False, + ) + + # Parent caches must allow direct invalidation + with self.assertRaisesRegex( + CacheInvalidDependencyError, + r"^Cache 'test_cache_6' cannot depend on cache 'test_cache_3':" + " 'test_cache_3' doesn't exist or doesn't allow direct invalidation$", + ): + add_custom_cache( + "test_cache_6", + 10, + depends_on_caches=["test_cache_3"], + allows_direct_invalidation=False, + ) + + @mute_logger("odoo.addons.base_registry_cache_custom.registry") + def test_07_add_custom_cache_ormcache_usage_and_invalidation(self): + """Checks usage of custom caches for the ``@ormcache`` decorator""" + add_custom_cache("test_cache_1", 10) + add_custom_cache( + "test_cache_2", + 10, + depends_on_caches=["test_cache_1"], + allows_direct_invalidation=True, + ) + + cls = type(self) + loader = FakeModelLoader(cls.env, cls.__module__) + loader.backup_registry() + + class Base(models.BaseModel): + _inherit = "base" + + @ormcache("param", cache="test_cache_1") + def func_1(self, param: str): + return tuple(self.ids), param + + @ormcache("param", cache="test_cache_2") + def func_2(self, param: str): + return tuple(self.ids), param + + loader.update_registry([Base]) + func_1 = self.env["base"].func_1 + func_2 = self.env["base"].func_2 + + # Prepare params to check + # NB: ``counter.hit|miss`` will count how many times the value has been + # retrieved from the cache (hit) or by executing the cached function (miss) + cache_1, key_1, counter_1 = get_cache_key_counter(func_1, "param-1") + hit_1 = counter_1.hit + miss_1 = counter_1.miss + cache_2, key_2, counter_2 = get_cache_key_counter(func_2, "param-2") + hit_2 = counter_2.hit + miss_2 = counter_2.miss + + # Clear parent cache to clear both caches + self.env.registry.clear_cache("test_cache_1") + self.assertNotIn(key_1, cache_1) + self.assertNotIn(key_2, cache_2) + + # Execute the functions, check counters + func_1("param-1") + self.assertEqual(counter_1.hit, hit_1) + self.assertEqual(counter_1.miss, miss_1 + 1) + self.assertIn(key_1, cache_1) + func_2("param-2") + self.assertEqual(counter_2.hit, hit_2) + self.assertEqual(counter_2.miss, miss_2 + 1) + self.assertIn(key_2, cache_2) + func_1("param-1") + self.assertEqual(counter_1.hit, hit_1 + 1) + self.assertEqual(counter_1.miss, miss_1 + 1) + self.assertIn(key_1, cache_1) + func_2("param-2") + self.assertEqual(counter_2.hit, hit_2 + 1) + self.assertEqual(counter_2.miss, miss_2 + 1) + self.assertIn(key_2, cache_2) + + # Clear parent cache to clear both caches (again) + self.env.registry.clear_cache("test_cache_1") + self.assertNotIn(key_1, cache_1) + self.assertNotIn(key_2, cache_2) + + # Execute the functions, check counters (again) + func_1("param-1") + self.assertEqual(counter_1.hit, hit_1 + 1) + self.assertEqual(counter_1.miss, miss_1 + 2) + self.assertIn(key_1, cache_1) + func_2("param-2") + self.assertEqual(counter_2.hit, hit_2 + 1) + self.assertEqual(counter_2.miss, miss_2 + 2) + self.assertIn(key_2, cache_2) + func_1("param-1") + self.assertEqual(counter_1.hit, hit_1 + 2) + self.assertEqual(counter_1.miss, miss_1 + 2) + self.assertIn(key_1, cache_1) + func_2("param-2") + self.assertEqual(counter_2.hit, hit_2 + 2) + self.assertEqual(counter_2.miss, miss_2 + 2) + self.assertIn(key_2, cache_2) + + # Clear dependent cache only this time + self.env.registry.clear_cache("test_cache_2") + self.assertIn(key_1, cache_1) + self.assertNotIn(key_2, cache_2) + + # Execute the functions, check counters (again): this time the ``hit`` value + # for the parent cache will increase when we run ``func_1`` again, while the + # ``miss`` value of the dependent cache should increase when calling ``func_2`` + func_1("param-1") + self.assertEqual(counter_1.hit, hit_1 + 3) + self.assertEqual(counter_1.miss, miss_1 + 2) + self.assertIn(key_1, cache_1) + func_2("param-2") + self.assertEqual(counter_2.hit, hit_2 + 2) + self.assertEqual(counter_2.miss, miss_2 + 3) + self.assertIn(key_2, cache_2) + + # Reset models registry + loader.restore_registry()