-
Notifications
You must be signed in to change notification settings - Fork 36
⚡ Bolt: O(1) Cache & Blockchain Optimization #489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3b688f3
7731c78
f2759df
0fe4b71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,60 +3,68 @@ | |||||||||||||||||||||||||||||||||||||||||||||
| import threading | ||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any, Optional | ||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import datetime, timedelta | ||||||||||||||||||||||||||||||||||||||||||||||
| from collections import OrderedDict | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| class ThreadSafeCache: | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| Thread-safe cache implementation with TTL and memory management. | ||||||||||||||||||||||||||||||||||||||||||||||
| Fixes race conditions and implements proper cache expiration. | ||||||||||||||||||||||||||||||||||||||||||||||
| Thread-safe cache implementation with TTL and O(1) LRU eviction. | ||||||||||||||||||||||||||||||||||||||||||||||
| Uses OrderedDict to maintain access order for efficient memory management. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, ttl: int = 300, max_size: int = 100): | ||||||||||||||||||||||||||||||||||||||||||||||
| self._data = {} | ||||||||||||||||||||||||||||||||||||||||||||||
| self._data = OrderedDict() | ||||||||||||||||||||||||||||||||||||||||||||||
| self._timestamps = {} | ||||||||||||||||||||||||||||||||||||||||||||||
| self._ttl = ttl # Time to live in seconds | ||||||||||||||||||||||||||||||||||||||||||||||
| self._max_size = max_size # Maximum number of cache entries | ||||||||||||||||||||||||||||||||||||||||||||||
| self._lock = threading.RLock() # Reentrant lock for thread safety | ||||||||||||||||||||||||||||||||||||||||||||||
| self._access_count = {} # Track access frequency for LRU eviction | ||||||||||||||||||||||||||||||||||||||||||||||
| self._hits = 0 | ||||||||||||||||||||||||||||||||||||||||||||||
| self._misses = 0 | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def get(self, key: str = "default") -> Optional[Any]: | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| Thread-safe get operation with automatic cleanup. | ||||||||||||||||||||||||||||||||||||||||||||||
| Thread-safe get operation with automatic cleanup and LRU update. | ||||||||||||||||||||||||||||||||||||||||||||||
| Performance: O(1) | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| with self._lock: | ||||||||||||||||||||||||||||||||||||||||||||||
| current_time = time.time() | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Check if key exists and is not expired | ||||||||||||||||||||||||||||||||||||||||||||||
| if key in self._data and key in self._timestamps: | ||||||||||||||||||||||||||||||||||||||||||||||
| if current_time - self._timestamps[key] < self._ttl: | ||||||||||||||||||||||||||||||||||||||||||||||
| # Update access count for LRU | ||||||||||||||||||||||||||||||||||||||||||||||
| self._access_count[key] = self._access_count.get(key, 0) + 1 | ||||||||||||||||||||||||||||||||||||||||||||||
| # Move to end (Mark as Most Recently Used) | ||||||||||||||||||||||||||||||||||||||||||||||
| self._data.move_to_end(key) | ||||||||||||||||||||||||||||||||||||||||||||||
| self._hits += 1 | ||||||||||||||||||||||||||||||||||||||||||||||
| return self._data[key] | ||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||
| # Expired entry - remove it | ||||||||||||||||||||||||||||||||||||||||||||||
| self._remove_key(key) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| self._misses += 1 | ||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def set(self, data: Any, key: str = "default") -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| Thread-safe set operation with memory management. | ||||||||||||||||||||||||||||||||||||||||||||||
| Thread-safe set operation with O(1) LRU eviction. | ||||||||||||||||||||||||||||||||||||||||||||||
| Performance: O(1) | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| with self._lock: | ||||||||||||||||||||||||||||||||||||||||||||||
| current_time = time.time() | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Clean up expired entries before adding new one | ||||||||||||||||||||||||||||||||||||||||||||||
| self._cleanup_expired() | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # If cache is full, evict least recently used entry | ||||||||||||||||||||||||||||||||||||||||||||||
| if len(self._data) >= self._max_size and key not in self._data: | ||||||||||||||||||||||||||||||||||||||||||||||
| # If key exists, move to end before updating | ||||||||||||||||||||||||||||||||||||||||||||||
| if key in self._data: | ||||||||||||||||||||||||||||||||||||||||||||||
| self._data.move_to_end(key) | ||||||||||||||||||||||||||||||||||||||||||||||
| # If cache is full and key is new, evict oldest (first) entry | ||||||||||||||||||||||||||||||||||||||||||||||
| elif len(self._data) >= self._max_size: | ||||||||||||||||||||||||||||||||||||||||||||||
| self._evict_lru() | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Set new data atomically | ||||||||||||||||||||||||||||||||||||||||||||||
| self._data[key] = data | ||||||||||||||||||||||||||||||||||||||||||||||
| self._timestamps[key] = current_time | ||||||||||||||||||||||||||||||||||||||||||||||
| self._access_count[key] = 1 | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| logger.debug(f"Cache set: key={key}, size={len(self._data)}") | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -75,7 +83,6 @@ def clear(self) -> None: | |||||||||||||||||||||||||||||||||||||||||||||
| with self._lock: | ||||||||||||||||||||||||||||||||||||||||||||||
| self._data.clear() | ||||||||||||||||||||||||||||||||||||||||||||||
| self._timestamps.clear() | ||||||||||||||||||||||||||||||||||||||||||||||
| self._access_count.clear() | ||||||||||||||||||||||||||||||||||||||||||||||
| logger.debug("Cache cleared") | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def get_stats(self) -> dict: | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -93,7 +100,9 @@ def get_stats(self) -> dict: | |||||||||||||||||||||||||||||||||||||||||||||
| "total_entries": len(self._data), | ||||||||||||||||||||||||||||||||||||||||||||||
| "expired_entries": expired_count, | ||||||||||||||||||||||||||||||||||||||||||||||
| "max_size": self._max_size, | ||||||||||||||||||||||||||||||||||||||||||||||
| "ttl_seconds": self._ttl | ||||||||||||||||||||||||||||||||||||||||||||||
| "ttl_seconds": self._ttl, | ||||||||||||||||||||||||||||||||||||||||||||||
| "hits": self._hits, | ||||||||||||||||||||||||||||||||||||||||||||||
| "misses": self._misses | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def _remove_key(self, key: str) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -103,14 +112,33 @@ def _remove_key(self, key: str) -> None: | |||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| self._data.pop(key, None) | ||||||||||||||||||||||||||||||||||||||||||||||
| self._timestamps.pop(key, None) | ||||||||||||||||||||||||||||||||||||||||||||||
| self._access_count.pop(key, None) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def _cleanup_expired(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| Internal method to clean up expired entries. | ||||||||||||||||||||||||||||||||||||||||||||||
| Must be called within lock context. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| current_time = time.time() | ||||||||||||||||||||||||||||||||||||||||||||||
| # Note: In OrderedDict, we could stop early if we encounter a non-expired item, | ||||||||||||||||||||||||||||||||||||||||||||||
| # but since items can be updated (moving to end), we stick to full scan or | ||||||||||||||||||||||||||||||||||||||||||||||
| # just check the oldest. However, multiple items can expire. | ||||||||||||||||||||||||||||||||||||||||||||||
| # Efficient cleanup: check from the beginning (oldest) | ||||||||||||||||||||||||||||||||||||||||||||||
| expired_keys = [] | ||||||||||||||||||||||||||||||||||||||||||||||
| for key in self._data: | ||||||||||||||||||||||||||||||||||||||||||||||
| if current_time - self._timestamps[key] >= self._ttl: | ||||||||||||||||||||||||||||||||||||||||||||||
| expired_keys.append(key) | ||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||
| # Since we move accessed/updated items to the end, | ||||||||||||||||||||||||||||||||||||||||||||||
| # we can't assume total temporal ordering here if TTL varies, | ||||||||||||||||||||||||||||||||||||||||||||||
| # but with fixed TTL, items at the front are older. | ||||||||||||||||||||||||||||||||||||||||||||||
| # Actually, move_to_end breaks strict temporal ordering of 'set' time. | ||||||||||||||||||||||||||||||||||||||||||||||
| # So we keep the list comprehension for safety or just check all. | ||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Re-evaluating: move_to_end is for LRU. If we want TTL to be efficient, | ||||||||||||||||||||||||||||||||||||||||||||||
| # we'd need another structure. But for max_size=100, full scan is fine. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # To be safe and simple: | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+122
to
+141
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Note: In OrderedDict, we could stop early if we encounter a non-expired item, | |
| # but since items can be updated (moving to end), we stick to full scan or | |
| # just check the oldest. However, multiple items can expire. | |
| # Efficient cleanup: check from the beginning (oldest) | |
| expired_keys = [] | |
| for key in self._data: | |
| if current_time - self._timestamps[key] >= self._ttl: | |
| expired_keys.append(key) | |
| else: | |
| # Since we move accessed/updated items to the end, | |
| # we can't assume total temporal ordering here if TTL varies, | |
| # but with fixed TTL, items at the front are older. | |
| # Actually, move_to_end breaks strict temporal ordering of 'set' time. | |
| # So we keep the list comprehension for safety or just check all. | |
| pass | |
| # Re-evaluating: move_to_end is for LRU. If we want TTL to be efficient, | |
| # we'd need another structure. But for max_size=100, full scan is fine. | |
| # To be safe and simple: | |
| # Full scan over timestamps is acceptable for typical cache sizes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: expired_keys is computed twice; the new loop is dead work because it’s overwritten by the list comprehension, adding unnecessary O(n) overhead on every cache write. Remove the redundant scan and keep a single computation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/cache.py, line 126:
<comment>`expired_keys` is computed twice; the new loop is dead work because it’s overwritten by the list comprehension, adding unnecessary O(n) overhead on every cache write. Remove the redundant scan and keep a single computation.</comment>
<file context>
@@ -103,14 +112,33 @@ def _remove_key(self, key: str) -> None:
+ # but since items can be updated (moving to end), we stick to full scan or
+ # just check the oldest. However, multiple items can expire.
+ # Efficient cleanup: check from the beginning (oldest)
+ expired_keys = []
+ for key in self._data:
+ if current_time - self._timestamps[key] >= self._ttl:
</file context>
| expired_keys = [] | |
| for key in self._data: | |
| if current_time - self._timestamps[key] >= self._ttl: | |
| expired_keys.append(key) | |
| else: | |
| # Since we move accessed/updated items to the end, | |
| # we can't assume total temporal ordering here if TTL varies, | |
| # but with fixed TTL, items at the front are older. | |
| # Actually, move_to_end breaks strict temporal ordering of 'set' time. | |
| # So we keep the list comprehension for safety or just check all. | |
| pass | |
| # Re-evaluating: move_to_end is for LRU. If we want TTL to be efficient, | |
| # we'd need another structure. But for max_size=100, full scan is fine. | |
| # To be safe and simple: | |
| expired_keys = [ | |
| # To be safe and simple: | |
| expired_keys = [ | |
| key for key, timestamp in self._timestamps.items() | |
| if current_time - timestamp >= self._ttl | |
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set()write path is still O(N) due unconditional TTL sweep.Calling
_cleanup_expired()on every write makessetlinear in cache size under lock, which can bring back contention under load.⚙️ Suggested adjustment
🤖 Prompt for AI Agents