Get cached value or compute it, preventing concurrent computation Args: key: Cache key compute_fn: Function to call if cache miss (should take no arguments) timeout_seconds: Cache timeout in seconds Returns: Cached or computed value
(self, key: str, compute_fn, timeout_seconds: int | None = None)
| 162 | del self._compute_locks[key] |
| 163 | |
| 164 | def get_or_compute(self, key: str, compute_fn, timeout_seconds: int | None = None) -> Any: |
| 165 | """Get cached value or compute it, preventing concurrent computation |
| 166 | |
| 167 | Args: |
| 168 | key: Cache key |
| 169 | compute_fn: Function to call if cache miss (should take no arguments) |
| 170 | timeout_seconds: Cache timeout in seconds |
| 171 | |
| 172 | Returns: |
| 173 | Cached or computed value |
| 174 | """ |
| 175 | # First check: see if we have cached value |
| 176 | cached_value = self.get(key, timeout_seconds) |
| 177 | if cached_value is not None: |
| 178 | logger.debug(f"Cache hit for key '{key}'") |
| 179 | return cached_value |
| 180 | |
| 181 | # Get per-key lock to prevent concurrent computation |
| 182 | compute_lock = self._get_compute_lock(key) |
| 183 | |
| 184 | # Check if we're waiting for another thread |
| 185 | if compute_lock.locked(): |
| 186 | logger.info(f"Cache key '{key}' is being computed by another request, waiting...") |
| 187 | |
| 188 | try: |
| 189 | with compute_lock: |
| 190 | # Second check: another thread might have computed it while we waited for lock |
| 191 | cached_value = self.get(key, timeout_seconds) |
| 192 | if cached_value is not None: |
| 193 | logger.info( |
| 194 | f"✓ Duplicate computation prevented for key '{key}' - using result from another request" |
| 195 | ) |
| 196 | return cached_value |
| 197 | |
| 198 | # Compute the value |
| 199 | logger.info(f"Cache miss for key '{key}', computing value...") |
| 200 | value = compute_fn() |
| 201 | |
| 202 | # Store in cache |
| 203 | self.set(key, value) |
| 204 | logger.info(f"✓ Computed and cached value for key '{key}'") |
| 205 | |
| 206 | return value |
| 207 | finally: |
| 208 | # Cleanup compute lock to avoid memory leak |
| 209 | self._cleanup_compute_lock(key) |
| 210 | |
| 211 | |
| 212 | # Global cache instance |
no test coverage detected