Skip to content

Commit 1893216

Browse files
Refactoring buffer class
1 parent 5406a77 commit 1893216

File tree

2 files changed

+162
-32
lines changed

2 files changed

+162
-32
lines changed

aws_lambda_powertools/logging/buffer/cache.py

Lines changed: 153 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,115 @@
77
from aws_lambda_powertools.warnings import PowertoolsUserWarning
88

99

10+
class KeyBufferCache:
11+
"""
12+
A cache implementation for a single key with size tracking and eviction support.
13+
14+
This class manages a buffer for a specific key, keeping track of the current size
15+
and providing methods to add, remove, and manage cached items. It supports automatic
16+
eviction tracking and size management.
17+
18+
Attributes
19+
----------
20+
cache : deque
21+
A double-ended queue storing the cached items.
22+
current_size : int
23+
The total size of all items currently in the cache.
24+
has_evicted : bool
25+
A flag indicating whether any items have been evicted from the cache.
26+
"""
27+
28+
def __init__(self):
29+
"""
30+
Initialize a buffer cache for a specific key.
31+
"""
32+
self.cache: deque = deque()
33+
self.current_size: int = 0
34+
self.has_evicted: bool = False
35+
36+
def add(self, item: Any) -> None:
37+
"""
38+
Add an item to the cache.
39+
40+
Parameters
41+
----------
42+
item : Any
43+
The item to be stored in the cache.
44+
"""
45+
item_size = len(str(item))
46+
self.cache.append(item)
47+
self.current_size += item_size
48+
49+
def remove_oldest(self) -> Any:
50+
"""
51+
Remove and return the oldest item from the cache.
52+
53+
Returns
54+
-------
55+
Any
56+
The removed item.
57+
"""
58+
removed_item = self.cache.popleft()
59+
self.current_size -= len(str(removed_item))
60+
self.has_evicted = True
61+
return removed_item
62+
63+
def get(self) -> list:
64+
"""
65+
Retrieve items for this key.
66+
67+
Returns
68+
-------
69+
list
70+
List of items in the cache.
71+
"""
72+
return list(self.cache)
73+
74+
def clear(self) -> None:
75+
"""
76+
Clear the cache for this key.
77+
"""
78+
self.cache.clear()
79+
self.current_size = 0
80+
self.has_evicted = False
81+
82+
1083
class LoggerBufferCache:
84+
"""
85+
A multi-key buffer cache with size-based eviction and management.
86+
87+
This class provides a flexible caching mechanism that manages multiple keys,
88+
with each key having its own buffer cache. The total size of each key's cache
89+
is limited, and older items are automatically evicted when the size limit is reached.
90+
91+
Key Features:
92+
- Multiple key support
93+
- Size-based eviction
94+
- Tracking of evicted items
95+
- Configurable maximum buffer size
96+
97+
Example
98+
--------
99+
>>> buffer_cache = LoggerBufferCache(max_size_bytes=1000)
100+
>>> buffer_cache.add("logs", "First log message")
101+
>>> buffer_cache.add("debug", "Debug information")
102+
>>> buffer_cache.get("logs")
103+
['First log message']
104+
>>> buffer_cache.get_current_size("logs")
105+
16
106+
"""
107+
11108
def __init__(self, max_size_bytes: int):
12109
"""
13110
Initialize the LoggerBufferCache.
14111
15112
Parameters
16113
----------
17114
max_size_bytes : int
18-
Maximum size of the cache in bytes.
115+
Maximum size of the cache in bytes for each key.
19116
"""
20117
self.max_size_bytes: int = max_size_bytes
21-
self.cache: dict[str, deque] = {}
22-
self.current_size: dict[str, int] = {}
23-
self.has_evicted: bool = False
118+
self.cache: dict[str, KeyBufferCache] = {}
24119

25120
def add(self, key: str, item: Any) -> None:
26121
"""
@@ -33,31 +128,34 @@ def add(self, key: str, item: Any) -> None:
33128
item : Any
34129
The item to be stored in the cache.
35130
36-
Notes
37-
-----
38-
If the item size exceeds the maximum cache size, it will not be added.
131+
Returns
132+
-------
133+
bool
134+
True if item was added, False otherwise.
39135
"""
136+
# Check if item is larger than entire buffer
40137
item_size = len(str(item))
41-
42138
if item_size > self.max_size_bytes:
43139
warnings.warn(
44-
message=f"Item size {item_size} bytes exceeds total cache size {self.max_size_bytes} bytes",
45-
category=PowertoolsUserWarning,
140+
f"Item size {item_size} bytes exceeds total cache size {self.max_size_bytes} bytes",
141+
PowertoolsUserWarning,
46142
stacklevel=2,
47143
)
48-
return
144+
return False
49145

146+
# Create the key's cache if it doesn't exist
50147
if key not in self.cache:
51-
self.cache[key] = deque()
52-
self.current_size[key] = 0
148+
self.cache[key] = KeyBufferCache()
53149

54-
while self.current_size[key] + item_size > self.max_size_bytes and self.cache[key]:
55-
removed_item = self.cache[key].popleft()
56-
self.current_size[key] -= len(str(removed_item))
57-
self.has_evicted = True
150+
# Calculate the size after adding the new item
151+
new_total_size = self.cache[key].current_size + item_size
58152

59-
self.cache[key].append(item)
60-
self.current_size[key] += item_size
153+
# If adding the item would exceed max size, remove oldest items
154+
while new_total_size > self.max_size_bytes and self.cache[key].cache:
155+
self.cache[key].remove_oldest()
156+
new_total_size = self.cache[key].current_size + item_size
157+
158+
self.cache[key].add(item)
61159

62160
def get(self, key: str) -> list:
63161
"""
@@ -73,21 +171,53 @@ def get(self, key: str) -> list:
73171
list
74172
List of items for the given key, or an empty list if the key doesn't exist.
75173
"""
76-
return list(self.cache.get(key, deque()))
174+
return [] if key not in self.cache else self.cache[key].get()
77175

78176
def clear(self, key: str | None = None) -> None:
79177
"""
80178
Clear the cache, either for a specific key or entirely.
81179
82180
Parameters
83181
----------
84-
key : str, optional
182+
key : Optional[str], optional
85183
The key to clear. If None, clears the entire cache.
86184
"""
87185
if key:
88186
if key in self.cache:
187+
self.cache[key].clear()
89188
del self.cache[key]
90-
del self.current_size[key]
91189
else:
92190
self.cache.clear()
93-
self.current_size.clear()
191+
192+
def has_evicted(self, key: str) -> bool:
193+
"""
194+
Check if a specific key's cache has evicted items.
195+
196+
Parameters
197+
----------
198+
key : str
199+
The key to check for evicted items.
200+
201+
Returns
202+
-------
203+
bool
204+
True if items have been evicted, False otherwise.
205+
"""
206+
return False if key not in self.cache else self.cache[key].has_evicted
207+
208+
def get_current_size(self, key: str) -> int | None:
209+
"""
210+
Get the current size of the buffer for a specific key.
211+
212+
Parameters
213+
----------
214+
key : str
215+
The key to get the current size for.
216+
217+
Returns
218+
-------
219+
int
220+
The current size of the buffer for the key.
221+
Returns 0 if the key does not exist.
222+
"""
223+
return None if key not in self.cache else self.cache[key].current_size

tests/unit/logger/required_dependencies/test_logger_buffer_cache.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ def test_initialization():
1212
# THEN cache should have correct initial state
1313
assert logger_cache.max_size_bytes == 1000
1414
assert logger_cache.cache == {}
15-
assert logger_cache.current_size == {}
1615

1716

1817
def test_add_single_item():
@@ -25,7 +24,7 @@ def test_add_single_item():
2524
# THEN item is stored correctly with proper size tracking
2625
assert len(logger_cache.get("key1")) == 1
2726
assert logger_cache.get("key1")[0] == "test_item"
28-
assert logger_cache.current_size["key1"] == len("test_item")
27+
assert logger_cache.get_current_size("key1") == len("test_item")
2928

3029

3130
def test_add_multiple_items_same_key():
@@ -39,6 +38,7 @@ def test_add_multiple_items_same_key():
3938
# THEN items are stored sequentially
4039
assert len(logger_cache.get("key1")) == 2
4140
assert logger_cache.get("key1") == ["item1", "item2"]
41+
assert logger_cache.has_evicted("key1") is False
4242

4343

4444
def test_cache_size_limit_single_key():
@@ -52,7 +52,8 @@ def test_cache_size_limit_single_key():
5252

5353
# THEN cache maintains size limit for a single key
5454
assert len(logger_cache.get("key1")) > 0
55-
assert logger_cache.current_size["key1"] <= 10
55+
assert logger_cache.get_current_size("key1") <= 10
56+
assert logger_cache.has_evicted("key1") is True
5657

5758

5859
def test_item_larger_than_cache():
@@ -104,7 +105,6 @@ def test_clear_all():
104105

105106
# THEN cache becomes empty
106107
assert logger_cache.cache == {}
107-
assert logger_cache.current_size == {}
108108

109109

110110
def test_clear_specific_key():
@@ -134,9 +134,9 @@ def test_multiple_keys_with_size_limits():
134134
logger_cache.add("key2", "long_item")
135135

136136
# THEN total size remains within limit
137-
assert len(logger_cache.cache["key1"]) > 0
138-
assert len(logger_cache.cache["key2"]) > 0
139-
assert logger_cache.current_size["key1"] + logger_cache.current_size["key2"] <= 20
137+
assert len(logger_cache.get("key1")) > 0
138+
assert len(logger_cache.get("key2")) > 0
139+
assert logger_cache.get_current_size("key1") + logger_cache.get_current_size("key2") <= 20
140140

141141

142142
def test_add_different_types():
@@ -162,5 +162,5 @@ def test_cache_size_tracking():
162162
logger_cache.add("key1", "another_item")
163163

164164
# THEN current size is tracked correctly
165-
assert logger_cache.current_size["key1"] == len("small") + len("another_item")
166-
assert logger_cache.current_size["key1"] <= 30
165+
assert logger_cache.get_current_size("key1") == len("small") + len("another_item")
166+
assert logger_cache.get_current_size("key1") <= 30

0 commit comments

Comments
 (0)