From 3be3b83c4f383d4e9b81cec2a285adfbb2c37de6 Mon Sep 17 00:00:00 2001 From: gavin Date: Fri, 27 May 2022 23:38:52 +0530 Subject: [PATCH] feat: frappe.utils.caching.site_cache Decorator to cache method calls across requests. The cache is stored in frappe.utils.caching._SITE_CACHE. The cache persists on the parent process. It offers a light-weight cache for the current process without the additional overhead of serializing / deserializing Python objects. Note: This cache isn't shared among workers. If you need to share data across workers, use redis (frappe.cache API) instead. Usage: from frappe.utils.caching import site_cache @site_cache def calculate_pi(): import math, time precision = get_precision("Math Constant", "Pi") # depends on site data return round(math.pi, precision) calculate_pi(10) # will calculate value calculate_pi(10) # will return value from cache calculate_pi.clear_cache() # clear this function's cache for all sites calculate_pi(10) # will calculate value --- frappe/utils/caching.py | 56 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/frappe/utils/caching.py b/frappe/utils/caching.py index d70e660b23..7adfe81606 100644 --- a/frappe/utils/caching.py +++ b/frappe/utils/caching.py @@ -2,17 +2,21 @@ # License: MIT. Check LICENSE import json +from collections import defaultdict from functools import wraps +from typing import Callable, Dict, Tuple import frappe +_SITE_CACHE = defaultdict(lambda: defaultdict(dict)) -def __generate_key(func, args, kwargs): + +def __generate_request_cache_key(func: Callable, args: Tuple, kwargs: Dict): """Generate a key for the cache.""" return f"{func.__module__}.{func.__name__}{json.dumps((args, kwargs))}" -def request_cache(func): +def request_cache(func: Callable) -> Callable: """Decorator to cache function calls mid-request. Cache is stored in frappe.local.request_cache. The cache only persists for the current request and is cleared when the request is over. The function is called just once @@ -42,7 +46,7 @@ def request_cache(func): logger = frappe.logger(module=__name__) try: - key = __generate_key(func, args, kwargs) + key = __generate_request_cache_key(func, args, kwargs) except Exception: logger.warning(f"request_cache: Couldn't generate key for args: {args}, kwargs: {kwargs}") return func(*args, **kwargs) @@ -54,3 +58,49 @@ def request_cache(func): return frappe.local.request_cache[key] return wrapper + + +def site_cache(func: Callable) -> Callable: + """Decorator to cache method calls across requests. The cache is stored in + frappe.utils.caching._SITE_CACHE. The cache persists on the parent process. + It offers a light-weight cache for the current process without the additional + overhead of serializing / deserializing Python objects. + + Note: This cache isn't shared among workers. If you need to share data across + workers, use redis (frappe.cache API) instead. + + Usage: + from frappe.utils.caching import site_cache + + @site_cache + def calculate_pi(): + import math, time + precision = get_precision("Math Constant", "Pi") # depends on site data + return round(math.pi, precision) + + calculate_pi(10) # will calculate value + calculate_pi(10) # will return value from cache + calculate_pi.clear_cache() # clear this function's cache for all sites + calculate_pi(10) # will calculate value + """ + func_key = f"{func.__module__}.{func.__name__}" + + def clear_cache(): + """Clear cache for this function for all sites if not specified.""" + _SITE_CACHE[func_key].clear() + + func.clear_cache = clear_cache + + @wraps(func) + def wrapper(*args, **kwargs): + if getattr(frappe.local, "initialised", None): + func_call_key = json.dumps((args, kwargs)) + + if func_call_key not in _SITE_CACHE[func_key][frappe.local.site]: + _SITE_CACHE[func_key][frappe.local.site][func_call_key] = func(*args, **kwargs) + + return _SITE_CACHE[func_key][frappe.local.site][func_call_key] + + return func(*args, **kwargs) + + return wrapper