pumpwood_communication.cache

Modules to manage a local disk cache for pumpwood requests.

1"""Modules to manage a local disk cache for pumpwood requests."""
2from .cache import PumpwoodCache, default_cache
3
4__docformat__ = "google"
5
6__all__ = [
7    PumpwoodCache, default_cache]
class PumpwoodCache:
 19class PumpwoodCache:
 20    """Class to implement local cache for Pumpwood Comunication requests."""
 21
 22    _INIT_LOCK = threading.Lock()
 23    """Lock to serialize FanoutCache initialization across threads."""
 24
 25    def __init__(self):
 26        """Initialize cache settings from configuration."""
 27        self._cache = None
 28        self._size_limit = CACHE_LIMIT_MB
 29        self._expire_time = CACHE_DEFAULT_EXPIRE
 30        self._transaction_timeout = CACHE_TRANSACTION_TIMEOUT
 31        self._n_shards = CACHE_N_SHARDS
 32        self._enable = CACHE_ENABLE
 33        self._retry_attempts = CACHE_RETRY_ATTEMPTS
 34        self._retry_delay = CACHE_RETRY_DELAY
 35        self._cache_path = (
 36            Path('/tmp/pumpwood_cache/') /
 37            CACHE_BASE_PATH)
 38
 39    def _build_fanout_cache(self) -> Optional[FanoutCache]:
 40        """Build FanoutCache with retries on SQLite lock contention.
 41
 42        Returns:
 43            Optional[FanoutCache]:
 44                Configured cache instance, or None when initialization
 45                fails after all retry attempts.
 46        """
 47        last_error = None
 48        for attempt in range(1, self._retry_attempts + 1):
 49            try:
 50                return FanoutCache(
 51                    directory=self._cache_path,
 52                    cache_size=self._size_limit,
 53                    tag_index=True,
 54                    timeout=self._transaction_timeout,
 55                    shards=self._n_shards)
 56            except (Timeout, sqlite3.OperationalError) as error:
 57                last_error = error
 58                if attempt >= self._retry_attempts:
 59                    break
 60                delay = self._retry_delay * attempt
 61                warning_msg = (
 62                    "Cache init locked, retry {attempt}/{total} "
 63                    "after {delay}s")
 64                logger.warning(
 65                    warning_msg.format(
 66                        attempt=attempt,
 67                        total=self._retry_attempts,
 68                        delay=delay))
 69                time.sleep(delay)
 70
 71        warning_msg = (
 72            "Cache init failed after {total} attempts: {error}")
 73        logger.warning(
 74            warning_msg.format(
 75                total=self._retry_attempts,
 76                error=last_error))
 77        return None
 78
 79    def refresh_cache(self) -> bool:
 80        """Refresh cache.
 81
 82        Returns:
 83            True if cache is refreshed.
 84        """
 85        self._cache = self._build_fanout_cache()
 86        return True
 87    
 88    def _create_cache_object(self) -> None:
 89        """Create FanoutCache once in a thread-safe way."""
 90        if self._cache is not None:
 91            return
 92        with self._INIT_LOCK:
 93            if self._cache is None:
 94                self._cache = self._build_fanout_cache()
 95
 96    def _execute_with_retry(self, operation: Callable[[], Any],
 97                            default: Any,
 98                            operation_name: str) -> Any:
 99        """Execute a cache operation with retries on lock contention.
100
101        Args:
102            operation (Callable[[], Any]):
103                Callable that performs the cache operation.
104            default (Any):
105                Value returned when all retries are exhausted or when
106                cache initialization fails.
107            operation_name (str):
108                Operation label used in warning logs.
109
110        Returns:
111            Any:
112                Result from ``operation``, or ``default`` on failure.
113        """
114        # Try to execute the operation with retries on lock contention.
115        last_error = None
116        for attempt in range(1, self._retry_attempts + 1):
117            try:
118                self._create_cache_object()
119                if self._cache is None:
120                    return default
121                return operation()
122            except (Timeout, sqlite3.OperationalError) as error:
123                last_error = error
124                if attempt >= self._retry_attempts:
125                    break
126                delay = self._retry_delay * attempt
127                warning_msg = (
128                    "Cache {operation} locked, retry {attempt}/{total} "
129                    "after {delay}s")
130                logger.warning(
131                    warning_msg.format(
132                        operation=operation_name,
133                        attempt=attempt,
134                        total=self._retry_attempts,
135                        delay=delay))
136                time.sleep(delay)
137
138        warning_msg = (
139            "Cache {operation} failed after {total} attempts: {error}")
140        logger.warning(
141            warning_msg.format(
142                operation=operation_name, total=self._retry_attempts,
143                error=last_error))
144        return default
145
146    @classmethod
147    def generate_hash(cls, hash_dict: dict) -> str:
148        """Generate a hash to be used to storage and retrieve cache.
149
150        It will use pumpJsonDump function from serializers to dump correctly
151        any complex data such as date, geometry and numpy.
152
153        Expose _generate_hash
154
155        Args:
156            hash_dict (dict):
157                A dictonary with information that will be used on hash.
158
159        Returns:
160            Return a hash that will be used as cache.
161        """
162        return cls._generate_hash(hash_dict=hash_dict)
163
164    @classmethod
165    def _generate_hash(cls, hash_dict: dict) -> str:
166        """Generate a hash to be used to storage and retrieve cache.
167
168        It will use pumpJsonDump function from serializers to dump correctly
169        any complex data such as date, geometry and numpy.
170
171        Args:
172            hash_dict (dict):
173                A dictonary with information that will be used on hash.
174
175        Returns:
176            Return a hash that will be used as cache.
177        """
178        str_hash_dict = pumpJsonDump(hash_dict, sort_keys=True)
179        return hashlib.sha512(str_hash_dict).hexdigest()
180
181    def clear(self) -> bool:
182        """Invalidate cache.
183
184        Returns:
185            True is ok.
186        """
187        return self._execute_with_retry(
188            operation=lambda: self._cache.clear(),
189            default=False, operation_name="clear")
190
191    def evict(self, tag_dict: dict) -> bool:
192        """Invalidate cache from a tag.
193
194        Returns:
195            True is ok.
196        """
197        if tag_dict is None:
198            msg = (
199                "At pumpwood_communication cache.evict tag_dict should not be "
200                "'None'. To envict all databse use clear function.")
201            raise PumpWoodCacheError(msg)
202
203        hash_str = self.generate_hash(hash_dict=tag_dict)
204        return self._execute_with_retry(
205            operation=lambda: self._cache.evict(hash_str),
206            default=False, operation_name="evict")
207
208    def get(self, hash_dict: dict) -> Any:
209        """Get a value from cache.
210
211        Args:
212            hash_dict (dict):
213                A dictonary with information that will be used on hash.
214
215        Returns:
216            Return the cached value or None if not found.
217        """
218        if not self._enable:
219            logger.info("Get cache not enable")
220            return None
221
222        # It cache time is set to 0, than disable cache,
223        # this is usefull for testing
224        if self._expire_time == 0:
225            return None
226
227        hash_str = self.generate_hash(hash_dict=hash_dict)
228        return self._execute_with_retry(
229            operation=lambda: self._cache.get(hash_str),
230            default=None, operation_name="get")
231
232    def set(self, hash_dict: dict, value: Any, expire: int = None,
233            tag_dict: dict = None) -> bool:
234        """Set cache value.
235
236        Args:
237            hash_dict (dict):
238                A dictonary with information that will be used on hash.
239            value (Any):
240                Value that will be set on diskcache.
241            expire (int):
242                Number of seconds that will be considered as expirity time.
243            tag_dict (dict):
244                Optional parameter to set a tag to cache. Tagged cache can be
245                envicted together using envict function.
246
247        Returns:
248            Return a boolean value
249        """
250        if not self._enable:
251            logger.info("Set cache not enable")
252            return True
253
254        if hash_dict is None:
255            msg = (
256                "At pumpwood_communication cache.set hash_dict should not be "
257                "'None'")
258            raise PumpWoodCacheError(msg)
259        expire_time = expire or self._expire_time
260        # Do not store cache if expire_time == 0
261        if expire_time == 0:
262            return True
263
264        hash_str = self.generate_hash(hash_dict=hash_dict)
265        tag_str = None
266        if tag_dict is not None:
267            tag_str = self.generate_hash(hash_dict=tag_dict)
268
269        def _do_set() -> bool:
270            return self._cache.set(
271                hash_str, value=value, expire=expire_time,
272                tag=tag_str)
273
274        try:
275            return self._execute_with_retry(
276                operation=_do_set, default=False,
277                operation_name="set")
278        except Exception as error:
279            msg = (
280                'Error when setting cache not associated with lock '
281                'contention. {error}')
282            raise PumpWoodCacheError(
283                message=msg, payload={'error': str(error)})

Class to implement local cache for Pumpwood Comunication requests.

PumpwoodCache()
25    def __init__(self):
26        """Initialize cache settings from configuration."""
27        self._cache = None
28        self._size_limit = CACHE_LIMIT_MB
29        self._expire_time = CACHE_DEFAULT_EXPIRE
30        self._transaction_timeout = CACHE_TRANSACTION_TIMEOUT
31        self._n_shards = CACHE_N_SHARDS
32        self._enable = CACHE_ENABLE
33        self._retry_attempts = CACHE_RETRY_ATTEMPTS
34        self._retry_delay = CACHE_RETRY_DELAY
35        self._cache_path = (
36            Path('/tmp/pumpwood_cache/') /
37            CACHE_BASE_PATH)

Initialize cache settings from configuration.

def refresh_cache(self) -> bool:
79    def refresh_cache(self) -> bool:
80        """Refresh cache.
81
82        Returns:
83            True if cache is refreshed.
84        """
85        self._cache = self._build_fanout_cache()
86        return True

Refresh cache.

Returns:

True if cache is refreshed.

@classmethod
def generate_hash(cls, hash_dict: dict) -> str:
146    @classmethod
147    def generate_hash(cls, hash_dict: dict) -> str:
148        """Generate a hash to be used to storage and retrieve cache.
149
150        It will use pumpJsonDump function from serializers to dump correctly
151        any complex data such as date, geometry and numpy.
152
153        Expose _generate_hash
154
155        Args:
156            hash_dict (dict):
157                A dictonary with information that will be used on hash.
158
159        Returns:
160            Return a hash that will be used as cache.
161        """
162        return cls._generate_hash(hash_dict=hash_dict)

Generate a hash to be used to storage and retrieve cache.

It will use pumpJsonDump function from serializers to dump correctly any complex data such as date, geometry and numpy.

Expose _generate_hash

Arguments:
  • hash_dict (dict): A dictonary with information that will be used on hash.
Returns:

Return a hash that will be used as cache.

def clear(self) -> bool:
181    def clear(self) -> bool:
182        """Invalidate cache.
183
184        Returns:
185            True is ok.
186        """
187        return self._execute_with_retry(
188            operation=lambda: self._cache.clear(),
189            default=False, operation_name="clear")

Invalidate cache.

Returns:

True is ok.

def evict(self, tag_dict: dict) -> bool:
191    def evict(self, tag_dict: dict) -> bool:
192        """Invalidate cache from a tag.
193
194        Returns:
195            True is ok.
196        """
197        if tag_dict is None:
198            msg = (
199                "At pumpwood_communication cache.evict tag_dict should not be "
200                "'None'. To envict all databse use clear function.")
201            raise PumpWoodCacheError(msg)
202
203        hash_str = self.generate_hash(hash_dict=tag_dict)
204        return self._execute_with_retry(
205            operation=lambda: self._cache.evict(hash_str),
206            default=False, operation_name="evict")

Invalidate cache from a tag.

Returns:

True is ok.

def get(self, hash_dict: dict) -> Any:
208    def get(self, hash_dict: dict) -> Any:
209        """Get a value from cache.
210
211        Args:
212            hash_dict (dict):
213                A dictonary with information that will be used on hash.
214
215        Returns:
216            Return the cached value or None if not found.
217        """
218        if not self._enable:
219            logger.info("Get cache not enable")
220            return None
221
222        # It cache time is set to 0, than disable cache,
223        # this is usefull for testing
224        if self._expire_time == 0:
225            return None
226
227        hash_str = self.generate_hash(hash_dict=hash_dict)
228        return self._execute_with_retry(
229            operation=lambda: self._cache.get(hash_str),
230            default=None, operation_name="get")

Get a value from cache.

Arguments:
  • hash_dict (dict): A dictonary with information that will be used on hash.
Returns:

Return the cached value or None if not found.

def set( self, hash_dict: dict, value: Any, expire: int = None, tag_dict: dict = None) -> bool:
232    def set(self, hash_dict: dict, value: Any, expire: int = None,
233            tag_dict: dict = None) -> bool:
234        """Set cache value.
235
236        Args:
237            hash_dict (dict):
238                A dictonary with information that will be used on hash.
239            value (Any):
240                Value that will be set on diskcache.
241            expire (int):
242                Number of seconds that will be considered as expirity time.
243            tag_dict (dict):
244                Optional parameter to set a tag to cache. Tagged cache can be
245                envicted together using envict function.
246
247        Returns:
248            Return a boolean value
249        """
250        if not self._enable:
251            logger.info("Set cache not enable")
252            return True
253
254        if hash_dict is None:
255            msg = (
256                "At pumpwood_communication cache.set hash_dict should not be "
257                "'None'")
258            raise PumpWoodCacheError(msg)
259        expire_time = expire or self._expire_time
260        # Do not store cache if expire_time == 0
261        if expire_time == 0:
262            return True
263
264        hash_str = self.generate_hash(hash_dict=hash_dict)
265        tag_str = None
266        if tag_dict is not None:
267            tag_str = self.generate_hash(hash_dict=tag_dict)
268
269        def _do_set() -> bool:
270            return self._cache.set(
271                hash_str, value=value, expire=expire_time,
272                tag=tag_str)
273
274        try:
275            return self._execute_with_retry(
276                operation=_do_set, default=False,
277                operation_name="set")
278        except Exception as error:
279            msg = (
280                'Error when setting cache not associated with lock '
281                'contention. {error}')
282            raise PumpWoodCacheError(
283                message=msg, payload={'error': str(error)})

Set cache value.

Arguments:
  • hash_dict (dict): A dictonary with information that will be used on hash.
  • value (Any): Value that will be set on diskcache.
  • expire (int): Number of seconds that will be considered as expirity time.
  • tag_dict (dict): Optional parameter to set a tag to cache. Tagged cache can be envicted together using envict function.
Returns:

Return a boolean value

PumpwoodCache object at 0x7f52bb336960>