From 835c7784f97793e878d3379f947eeb28ad5a7cb4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 20 Nov 2025 22:31:33 +1100 Subject: [PATCH] Implement caching for unit registry: (#10870) * Implement caching for unit registry: - Registry could become out of sync across sessions - Implement a simple caching system * Simplify code --- src/backend/InvenTree/InvenTree/conversion.py | 64 +++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/conversion.py b/src/backend/InvenTree/InvenTree/conversion.py index 7d54a6aa11..42274ca552 100644 --- a/src/backend/InvenTree/InvenTree/conversion.py +++ b/src/backend/InvenTree/InvenTree/conversion.py @@ -2,30 +2,76 @@ import logging import re +from hashlib import md5 from typing import Optional from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ import pint - -_unit_registry = None - import structlog +from common.settings import get_global_setting, set_global_setting +from InvenTree.cache import get_session_cache, set_session_cache + +_UNIT_REG_CACHE_KEY = 'unit_registry_hash' +_unit_registry = None +_unit_registry_hash: str = '' + logger = structlog.get_logger('inventree') # Disable log output for Pint library logging.getLogger('pint').setLevel(logging.ERROR) +def get_unit_registry_hash(): + """Return a hash representing the current state of the unit registry. + + We use this to determine if we need to reload the unit registry, + due to changes in the database. + """ + # Look in the session cache first (faster, and potentially newer) + registry_hash = get_session_cache(_UNIT_REG_CACHE_KEY) + + if registry_hash is None: + registry_hash = get_global_setting( + '_UNIT_REGISTRY_HASH', create=False, backup_value='' + ) + + if registry_hash: + set_session_cache(_UNIT_REG_CACHE_KEY, registry_hash) + + return registry_hash + + +def set_unit_registry_hash(registry_hash: str): + """Save the hash representing the current state of the unit registry. + + Because most of the registry is static, we only need to consider the + CustomUnit entries in the database. + """ + global _unit_registry_hash + _unit_registry_hash = registry_hash + + # Save to both the global settings and the session cache + set_global_setting('_UNIT_REGISTRY_HASH', registry_hash) + set_session_cache(_UNIT_REG_CACHE_KEY, registry_hash) + + def get_unit_registry(): """Return a custom instance of the Pint UnitRegistry.""" global _unit_registry + global _unit_registry_hash # Cache the unit registry for speedier access if _unit_registry is None: return reload_unit_registry() + + # Check if the unit registry has changed + if _unit_registry_hash != get_unit_registry_hash(): + logger.info('Unit registry hash has changed, reloading unit registry') + return reload_unit_registry() + return _unit_registry @@ -60,9 +106,16 @@ def reload_unit_registry(): try: from common.models import CustomUnit + # Calculate a hash of all custom units + hash_md5 = md5() + for cu in CustomUnit.objects.all(): try: - reg.define(cu.fmt_string()) + fmt = cu.fmt_string() + reg.define(fmt) + + hash_md5.update(fmt.encode('utf-8')) + except Exception as e: logger.exception( 'Failed to load custom unit: %s - %s', cu.fmt_string(), e @@ -71,6 +124,9 @@ def reload_unit_registry(): # Once custom units are loaded, save registry _unit_registry = reg + # Update the unit registry hash + set_unit_registry_hash(hash_md5.hexdigest()) + except Exception: # Database is not ready, or CustomUnit model is not available pass