Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions base_registry_cache_custom/README.rst
Original file line number Diff line number Diff line change
@@ -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 <https://github.com/OCA/server-tools/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 <https://github.com/OCA/server-tools/issues/new?body=module:%20base_registry_cache_custom%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Authors
-------

* Camptocamp

Contributors
------------

- Silvio Gregorini <[email protected]>

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 <https://github.com/OCA/server-tools/tree/17.0/base_registry_cache_custom>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Empty file.
14 changes: 14 additions & 0 deletions base_registry_cache_custom/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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,
}
22 changes: 22 additions & 0 deletions base_registry_cache_custom/exceptions.py
Original file line number Diff line number Diff line change
@@ -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"""
3 changes: 3 additions & 0 deletions base_registry_cache_custom/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
7 changes: 7 additions & 0 deletions base_registry_cache_custom/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -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
```
1 change: 1 addition & 0 deletions base_registry_cache_custom/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Silvio Gregorini \<<[email protected]>\>
1 change: 1 addition & 0 deletions base_registry_cache_custom/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This module allows adding custom caches to the DBs' registries.
36 changes: 36 additions & 0 deletions base_registry_cache_custom/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -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)
```
122 changes: 122 additions & 0 deletions base_registry_cache_custom/registry.py
Original file line number Diff line number Diff line change
@@ -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()
Loading