cacheado 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cache.py +678 -0
- cache_policies/__init__.py +0 -0
- cache_policies/cache_policy_manager.py +187 -0
- cache_scopes/__init__.py +0 -0
- cache_scopes/scope_config.py +228 -0
- cache_types.py +6 -0
- cacheado-1.0.1.dist-info/METADATA +553 -0
- cacheado-1.0.1.dist-info/RECORD +21 -0
- cacheado-1.0.1.dist-info/WHEEL +5 -0
- cacheado-1.0.1.dist-info/licenses/LICENSE +21 -0
- cacheado-1.0.1.dist-info/top_level.txt +7 -0
- eviction_policies/__init__.py +0 -0
- eviction_policies/lre_eviction.py +130 -0
- protocols/__init__.py +0 -0
- protocols/cache.py +183 -0
- protocols/cache_policy_manager_protocol.py +82 -0
- protocols/eviction_policy.py +70 -0
- protocols/scope.py +85 -0
- protocols/storage_provider.py +67 -0
- storages/__init__.py +0 -0
- storages/in_memory.py +109 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
from cache_types import _CacheKey
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from cache import Cache
|
|
10
|
+
|
|
11
|
+
from protocols.cache_policy_manager_protocol import ICachePolicyManager
|
|
12
|
+
from protocols.eviction_policy import IEvictionPolicy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CachePolicyManager(ICachePolicyManager):
|
|
16
|
+
"""
|
|
17
|
+
Manages cache maintenance policies (e.g., eviction, cleanup).
|
|
18
|
+
|
|
19
|
+
This class handles background cleanup and delegates eviction logic
|
|
20
|
+
to an injected IEvictionPolicy. Optimized with __slots__ for memory efficiency.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__slots__ = ("_cache", "_cleanup_interval", "_policy", "_global_max_size", "_stop_event", "_cleanup_thread")
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self, cache_instance: "Cache", cleanup_interval: int, policy: IEvictionPolicy, max_size: Optional[int] = None
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initializes the policy manager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
cache_instance (Cache): The main Cache instance.
|
|
33
|
+
cleanup_interval (int): The interval (in seconds) for the cleanup loop.
|
|
34
|
+
policy (IEvictionPolicy): The injected eviction policy (e.g., LRUPolicy).
|
|
35
|
+
max_size (Optional[int]): The maximum number of items allowed globally.
|
|
36
|
+
"""
|
|
37
|
+
self._cache = cache_instance
|
|
38
|
+
self._cleanup_interval = cleanup_interval
|
|
39
|
+
self._policy = policy
|
|
40
|
+
self._global_max_size = max_size
|
|
41
|
+
self._stop_event = threading.Event()
|
|
42
|
+
self._cleanup_thread: Optional[threading.Thread] = None
|
|
43
|
+
logging.info(
|
|
44
|
+
f"CachePolicyManager initialized with policy={policy.__class__.__name__}, "
|
|
45
|
+
f"max_size={max_size}, cleanup_interval={cleanup_interval}s"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def start_background_cleanup(self) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Starts the background daemon thread for cache cleanup.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
if self._cleanup_thread is None or not self._cleanup_thread.is_alive():
|
|
54
|
+
self._stop_event.clear()
|
|
55
|
+
self._cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True, name="CacheCleanupThread")
|
|
56
|
+
self._cleanup_thread.start()
|
|
57
|
+
logging.info("Cache cleanup thread started.")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logging.error(f"Failed to start cache cleanup thread: {e}")
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
def stop_background_cleanup(self) -> None:
|
|
63
|
+
"""Stops the background cleanup thread gracefully."""
|
|
64
|
+
try:
|
|
65
|
+
if self._cleanup_thread and self._cleanup_thread.is_alive():
|
|
66
|
+
self._stop_event.set()
|
|
67
|
+
self._cleanup_thread.join()
|
|
68
|
+
logging.info("Cache cleanup thread stopped.")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logging.error(f"Error stopping cache cleanup thread: {e}")
|
|
71
|
+
|
|
72
|
+
def _cleanup_loop(self) -> None:
|
|
73
|
+
"""
|
|
74
|
+
The main loop for the garbage collector thread.
|
|
75
|
+
|
|
76
|
+
Periodically scans keys and triggers passive eviction for expired items.
|
|
77
|
+
"""
|
|
78
|
+
while not self._stop_event.wait(self._cleanup_interval):
|
|
79
|
+
try:
|
|
80
|
+
if self._cache is None:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
all_keys = self._cache._get_all_keys_from_storage()
|
|
84
|
+
if not all_keys:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
logging.info(f"Background cleanup: checking {len(all_keys)} keys.")
|
|
88
|
+
|
|
89
|
+
current_time = time.monotonic()
|
|
90
|
+
expired_count = 0
|
|
91
|
+
|
|
92
|
+
for key in all_keys:
|
|
93
|
+
value_tuple = self._cache._get_value_no_lock_from_storage(key)
|
|
94
|
+
if value_tuple and current_time > value_tuple[1]:
|
|
95
|
+
namespace = key[1]
|
|
96
|
+
self._cache._internal_get(key, namespace)
|
|
97
|
+
expired_count += 1
|
|
98
|
+
|
|
99
|
+
if expired_count > 0:
|
|
100
|
+
logging.info(f"Background cleanup: {expired_count} expired keys removed.")
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logging.error(f"Error in cache cleanup thread: {e}", exc_info=True)
|
|
104
|
+
|
|
105
|
+
def notify_set(self, key: _CacheKey, namespace: str, max_items: Optional[int]) -> Optional[_CacheKey]:
|
|
106
|
+
"""
|
|
107
|
+
Delegates 'set' notification to the eviction policy.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
key (_CacheKey): The key that was set.
|
|
111
|
+
namespace (str): The namespace of the key.
|
|
112
|
+
max_items (Optional[int]): The max_items limit for this namespace.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Optional[_CacheKey]: A key to evict, or None.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
return self._policy.notify_set(key, namespace, max_items, self._global_max_size)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logging.error(f"Error in policy notify_set: {e}")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def notify_get(self, key: _CacheKey, namespace: str) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Delegates 'get' notification to the eviction policy.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
key (_CacheKey): The key that was accessed.
|
|
129
|
+
namespace (str): The namespace of the key.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
self._policy.notify_get(key, namespace)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logging.error(f"Error in policy notify_get: {e}")
|
|
135
|
+
|
|
136
|
+
def notify_evict(self, key: _CacheKey, namespace: str) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Delegates 'evict' notification to the eviction policy.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
key (_CacheKey): The key that was evicted.
|
|
142
|
+
namespace (str): The namespace of the key.
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
self._policy.notify_evict(key, namespace)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logging.error(f"Error in policy notify_evict: {e}")
|
|
148
|
+
|
|
149
|
+
def notify_clear(self) -> None:
|
|
150
|
+
"""Delegates 'clear' notification to the eviction policy."""
|
|
151
|
+
try:
|
|
152
|
+
self._policy.notify_clear()
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logging.error(f"Error in policy notify_clear: {e}")
|
|
155
|
+
|
|
156
|
+
def get_namespace_count(self) -> int:
|
|
157
|
+
"""
|
|
158
|
+
Gets the total number of tracked namespaces from the policy.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
int: The count of namespaces.
|
|
162
|
+
"""
|
|
163
|
+
return self._policy.get_namespace_count()
|
|
164
|
+
|
|
165
|
+
def get_global_size(self) -> int:
|
|
166
|
+
"""
|
|
167
|
+
Gets the global item count from the policy.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
int: The global item count.
|
|
171
|
+
"""
|
|
172
|
+
return self._policy.get_global_size()
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def policy(self) -> IEvictionPolicy:
|
|
176
|
+
"""Returns the eviction policy instance."""
|
|
177
|
+
return self._policy
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def global_max_size(self) -> Optional[int]:
|
|
181
|
+
"""Returns the global maximum cache size."""
|
|
182
|
+
return self._global_max_size
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def cleanup_interval(self) -> int:
|
|
186
|
+
"""Returns the cleanup interval in seconds."""
|
|
187
|
+
return self._cleanup_interval
|
cache_scopes/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from protocols.scope import IScope
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ScopeLevel:
|
|
9
|
+
"""Represents a level in the scope hierarchy."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
param_name: str
|
|
13
|
+
children: Optional[List["ScopeLevel"]] = None
|
|
14
|
+
|
|
15
|
+
def __post_init__(self):
|
|
16
|
+
if not self.name:
|
|
17
|
+
raise ValueError("name cannot be empty")
|
|
18
|
+
if self.children is None:
|
|
19
|
+
self.children = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ScopeConfig(IScope):
|
|
23
|
+
"""
|
|
24
|
+
Hierarchical scope configuration for cache with multiple tree support.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
``` python
|
|
28
|
+
# Multiple independent trees
|
|
29
|
+
org_tree = ScopeLevel("organization", "org_id", [
|
|
30
|
+
ScopeLevel("user", "user_id")
|
|
31
|
+
])
|
|
32
|
+
car_tree = ScopeLevel("car", "car_id", [
|
|
33
|
+
ScopeLevel("door", "door_id", [
|
|
34
|
+
ScopeLevel("tire", "tire_id")
|
|
35
|
+
])
|
|
36
|
+
])
|
|
37
|
+
config = ScopeConfig([org_tree, car_tree])
|
|
38
|
+
```
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
__slots__ = ("_root_levels", "_all_levels", "_level_names", "_param_mapping")
|
|
42
|
+
|
|
43
|
+
def __init__(self, root_levels: Optional[List[ScopeLevel]] = None):
|
|
44
|
+
"""
|
|
45
|
+
Initializes scope configuration with global as implicit root.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
root_levels: List of global child levels (optional).
|
|
49
|
+
"""
|
|
50
|
+
global_level = ScopeLevel("global", "", root_levels or [])
|
|
51
|
+
|
|
52
|
+
self._root_levels = [global_level]
|
|
53
|
+
self._all_levels = self._flatten_levels(self._root_levels)
|
|
54
|
+
self._level_names = [level.name for level in self._all_levels]
|
|
55
|
+
self._param_mapping = {level.name: level.param_name for level in self._all_levels}
|
|
56
|
+
|
|
57
|
+
if len(set(self._level_names)) != len(self._level_names):
|
|
58
|
+
raise ValueError("Scope level names must be unique")
|
|
59
|
+
|
|
60
|
+
def _flatten_levels(self, levels: List[ScopeLevel]) -> List[ScopeLevel]:
|
|
61
|
+
"""Flattens the level tree into a list."""
|
|
62
|
+
result = []
|
|
63
|
+
for level in levels:
|
|
64
|
+
result.append(level)
|
|
65
|
+
if level.children:
|
|
66
|
+
result.extend(self._flatten_levels(level.children))
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def root_levels(self) -> List[ScopeLevel]:
|
|
71
|
+
"""Returns the configured root scope levels."""
|
|
72
|
+
return self._root_levels.copy()
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def all_levels(self) -> List[ScopeLevel]:
|
|
76
|
+
"""Returns all scope levels (flattened)."""
|
|
77
|
+
return self._all_levels.copy()
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def level_names(self) -> List[str]:
|
|
81
|
+
"""Returns the scope level names."""
|
|
82
|
+
return self._level_names.copy()
|
|
83
|
+
|
|
84
|
+
def get_param_name(self, level_name: str) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Returns the parameter name for a specific level.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
level_name: Name of the scope level.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Corresponding parameter name.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If the level doesn't exist.
|
|
96
|
+
"""
|
|
97
|
+
if level_name not in self._param_mapping:
|
|
98
|
+
raise ValueError(f"Unknown scope level: {level_name}")
|
|
99
|
+
return self._param_mapping[level_name]
|
|
100
|
+
|
|
101
|
+
def build_scope_path(self, scope_params: Dict[str, Any]) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Builds the scope path based on provided parameters.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
scope_params: Dictionary with scope parameters.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
String representing the hierarchical scope path.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def build_path_recursive(levels: List[ScopeLevel], path_parts: List[str]) -> List[str]:
|
|
113
|
+
for level in levels:
|
|
114
|
+
if level.name == "global":
|
|
115
|
+
if level.children:
|
|
116
|
+
return build_path_recursive(level.children, path_parts)
|
|
117
|
+
return path_parts
|
|
118
|
+
|
|
119
|
+
param_value = scope_params.get(level.param_name)
|
|
120
|
+
if param_value is not None:
|
|
121
|
+
new_path = path_parts + [f"{level.name}:{param_value}"]
|
|
122
|
+
if level.children:
|
|
123
|
+
child_path = build_path_recursive(level.children, new_path)
|
|
124
|
+
if len(child_path) > len(new_path):
|
|
125
|
+
return child_path
|
|
126
|
+
return new_path
|
|
127
|
+
return path_parts
|
|
128
|
+
|
|
129
|
+
path_parts = build_path_recursive(self._root_levels, [])
|
|
130
|
+
return "/".join(path_parts) if path_parts else "global"
|
|
131
|
+
|
|
132
|
+
def validate_scope_params(self, target_level: str, scope_params: Dict[str, Any]) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Validates that required parameters are present for the target level.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
target_level: Desired scope level.
|
|
138
|
+
scope_params: Provided parameters.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If mandatory parameters are missing.
|
|
142
|
+
"""
|
|
143
|
+
if target_level == "global":
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if target_level not in self._level_names:
|
|
147
|
+
raise ValueError(f"Unknown scope level: {target_level}")
|
|
148
|
+
|
|
149
|
+
path_to_target = self._find_path_to_level(target_level)
|
|
150
|
+
if not path_to_target:
|
|
151
|
+
raise ValueError(f"Cannot find path to scope level: {target_level}")
|
|
152
|
+
|
|
153
|
+
for level in path_to_target:
|
|
154
|
+
if level.param_name and (level.param_name not in scope_params or scope_params[level.param_name] is None):
|
|
155
|
+
raise ValueError(f"Missing required parameter '{level.param_name}' for scope level '{level.name}'")
|
|
156
|
+
|
|
157
|
+
def _find_path_to_level(self, target_level: str) -> Optional[List[ScopeLevel]]:
|
|
158
|
+
"""Finds the hierarchical path to a specific level."""
|
|
159
|
+
|
|
160
|
+
def search_recursive(levels: List[ScopeLevel], path: List[ScopeLevel]) -> Optional[List[ScopeLevel]]:
|
|
161
|
+
for level in levels:
|
|
162
|
+
current_path = path + [level]
|
|
163
|
+
if level.name == target_level:
|
|
164
|
+
return current_path
|
|
165
|
+
if level.children:
|
|
166
|
+
result = search_recursive(level.children, current_path)
|
|
167
|
+
if result:
|
|
168
|
+
return result
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
return search_recursive(self._root_levels, [])
|
|
172
|
+
|
|
173
|
+
def get_parent_scope_path(self, scope_path: str) -> Optional[str]:
|
|
174
|
+
"""
|
|
175
|
+
Returns the parent scope path.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
scope_path: Current scope path.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Parent scope path or None if global.
|
|
182
|
+
"""
|
|
183
|
+
if scope_path == "global":
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
parts = scope_path.split("/")
|
|
187
|
+
if len(parts) <= 1:
|
|
188
|
+
return "global"
|
|
189
|
+
|
|
190
|
+
return "/".join(parts[:-1])
|
|
191
|
+
|
|
192
|
+
def is_descendant_of(self, child_path: str, parent_path: str) -> bool:
|
|
193
|
+
"""
|
|
194
|
+
Checks if one scope is a descendant of another.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
child_path: Child scope path.
|
|
198
|
+
parent_path: Parent scope path.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
True if child_path is descendant of parent_path.
|
|
202
|
+
"""
|
|
203
|
+
if parent_path == "global":
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
if child_path == "global":
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
return child_path.startswith(parent_path + "/") or child_path == parent_path
|
|
210
|
+
|
|
211
|
+
def get_scope_tree_for_level(self, level_name: str) -> Optional[ScopeLevel]:
|
|
212
|
+
"""Returns the root tree that contains the specified level."""
|
|
213
|
+
|
|
214
|
+
def find_root(levels: List[ScopeLevel], target: str) -> Optional[ScopeLevel]:
|
|
215
|
+
for root in levels:
|
|
216
|
+
if self._level_exists_in_tree(root, target):
|
|
217
|
+
return root
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
return find_root(self._root_levels, level_name)
|
|
221
|
+
|
|
222
|
+
def _level_exists_in_tree(self, root: ScopeLevel, target: str) -> bool:
|
|
223
|
+
"""Checks if a level exists in the tree."""
|
|
224
|
+
if root.name == target:
|
|
225
|
+
return True
|
|
226
|
+
if root.children:
|
|
227
|
+
return any(self._level_exists_in_tree(child, target) for child in root.children)
|
|
228
|
+
return False
|