Skip to content

Commit 9adde49

Browse files
committed
refactor: use expire_at in ll instead of time of creation, replace time() with monotonic()
1 parent e68f4fd commit 9adde49

7 files changed

Lines changed: 42 additions & 38 deletions

File tree

tests/test_delete.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def test_delete():
1717
value = 2
1818
time_ = 10
1919

20-
node = DoubleLinkedListNode(value=_LinkedListValue(time_=time_, key=key))
20+
node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=time_, key=key))
2121
item = _DictValue(node=node, value=value)
2222
d._dict[key] = item
2323
d._ll_head = node
@@ -54,7 +54,7 @@ def test_delete__item_expired():
5454
value = 2
5555
time_ = 10
5656

57-
node = DoubleLinkedListNode(value=_LinkedListValue(time_=time_, key=key))
57+
node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=time_, key=key))
5858
d._dict[key] = _DictValue(node=node, value=value)
5959
d._ll_head = node
6060
d._ll_end = node

tests/test_get.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,21 @@
1010

1111
@pytest.mark.parametrize("update_ttl_on_get", [True, False])
1212
def test_get(update_ttl_on_get: bool):
13-
d = TTLMap(ttl=timedelta(seconds=1000), update_ttl_on_get=update_ttl_on_get)
13+
ttl = timedelta(seconds=1000)
14+
d = TTLMap(ttl=ttl, update_ttl_on_get=update_ttl_on_get)
1415
lock_mock = LockMock()
1516
d._lock = lock_mock
1617
key = 1
1718
value = 2
1819
time_ = 10
1920

20-
node = DoubleLinkedListNode(value=_LinkedListValue(time_=time_, key=key))
21+
node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=time_, key=key))
2122
d._dict[key] = _DictValue(node=node, value=value)
2223
d._ll_head = node
2324
d._ll_end = node
2425

2526
with (
26-
patch("time.time", return_value=time_) as time_mock,
27+
patch("time.monotonic", return_value=time_) as time_mock,
2728
patch.object(
2829
TTLMap,
2930
"_setitem",
@@ -35,7 +36,7 @@ def test_get(update_ttl_on_get: bool):
3536
time_mock.assert_called_once()
3637
update_by_ttl_mock.assert_called_once_with(current_time=time_)
3738
if update_ttl_on_get:
38-
setitem_mock.assert_called_once_with(key, value, time_)
39+
setitem_mock.assert_called_once_with(key, value, time_ + ttl.total_seconds())
3940
else:
4041
setitem_mock.assert_not_called()
4142

@@ -54,11 +55,11 @@ def test_get__item_expired():
5455
value = 2
5556
time_ = 10
5657

57-
node = DoubleLinkedListNode(value=_LinkedListValue(time_=time_, key=key))
58+
node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=time_, key=key))
5859
d._dict[key] = _DictValue(node=node, value=value)
5960
d._ll_head = node
6061
d._ll_end = node
6162

62-
with patch("time.time", return_value=time_ + ttl.total_seconds() + 1): # noqa: SIM117
63+
with patch("time.monotonic", return_value=time_ + ttl.total_seconds() + 1): # noqa: SIM117
6364
with pytest.raises(KeyError):
6465
_ = d[key]

tests/test_put_node_to_end.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def test_put_node_to_end__empty_dict():
99
d = TTLMap(ttl=timedelta(seconds=100))
1010
assert d._ll_head is None
1111
assert d._ll_end is None
12-
node = DoubleLinkedListNode(value=_LinkedListValue(time_=1, key=1))
12+
node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=1, key=1))
1313
d._put_node_to_end(node=node)
1414

1515
assert d._ll_head is node
@@ -31,7 +31,7 @@ def test_put_node_to_end__not_empty_dict():
3131
assert head_node is d._dict[1].node
3232
assert end_node is d._dict[2].node
3333

34-
node = DoubleLinkedListNode(value=_LinkedListValue(time_=1, key=3))
34+
node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=1, key=3))
3535
d._put_node_to_end(node=node)
3636

3737
assert d._ll_head is head_node

tests/test_set.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88

99

1010
def test_set__first():
11-
d = TTLMap(ttl=timedelta(seconds=1000))
11+
ttl = timedelta(seconds=1000)
12+
d = TTLMap(ttl=ttl)
1213
lock_mock = LockMock()
1314
d._lock = lock_mock
1415
key = 1
1516
value = 2
1617
time_ = 10
17-
expected_node = DoubleLinkedListNode(value=_LinkedListValue(time_=time_, key=key))
18+
expected_node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=time_ + ttl.total_seconds(), key=key))
1819
with (
19-
patch("time.time", return_value=time_) as time_mock,
20+
patch("time.monotonic", return_value=time_) as time_mock,
2021
patch.object(
2122
TTLMap,
2223
"_setitem",
@@ -35,7 +36,7 @@ def test_set__first():
3536
):
3637
d[key] = value
3738
time_mock.assert_called_once()
38-
setitem_mock.assert_called_once_with(key, value, time_)
39+
setitem_mock.assert_called_once_with(key, value, time_ + ttl.total_seconds())
3940
update_by_ttl_mock.assert_called_once_with(current_time=time_)
4041
update_by_size_mock.assert_called_once()
4142
lock_mock.__enter__.assert_called_once()
@@ -47,7 +48,8 @@ def test_set__first():
4748

4849

4950
def test_set__second():
50-
d = TTLMap(ttl=timedelta(seconds=1000))
51+
ttl = timedelta(seconds=1000)
52+
d = TTLMap(ttl=ttl)
5153
head_key = 1
5254
head_value = 2
5355
d[head_key] = head_value
@@ -57,9 +59,9 @@ def test_set__second():
5759
new_key = 5
5860
new_value = 20
5961
time_ = 10
60-
expected_node = DoubleLinkedListNode(value=_LinkedListValue(time_=time_, key=new_key))
62+
expected_node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=time_ + ttl.total_seconds(), key=new_key))
6163
with (
62-
patch("time.time", return_value=time_) as time_mock,
64+
patch("time.monotonic", return_value=time_) as time_mock,
6365
patch.object(
6466
TTLMap,
6567
"_setitem",
@@ -78,7 +80,7 @@ def test_set__second():
7880
):
7981
d[new_key] = new_value
8082
time_mock.assert_called_once()
81-
setitem_mock.assert_called_once_with(new_key, new_value, time_)
83+
setitem_mock.assert_called_once_with(new_key, new_value, time_ + ttl.total_seconds())
8284
update_by_ttl_mock.assert_called_once_with(current_time=time_)
8385
update_by_size_mock.assert_called_once()
8486
lock_mock.__enter__.assert_called_once()

tests/test_setitem.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def test_setitem__new_item():
1111
time_ = 100.0
1212
key = 1
1313
value = 1
14-
expected_node = DoubleLinkedListNode(value=_LinkedListValue(time_=time_, key=key))
14+
expected_node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=time_, key=key))
1515
with (
1616
patch.object(TTLMap, "_pop_ll_node") as mock_pop_ll_node,
1717
patch.object(
@@ -33,7 +33,7 @@ def test_setitem__existing_item():
3333
value = 1
3434
d[key] = value
3535
old_node = d._dict[key].node
36-
expected_node = DoubleLinkedListNode(value=_LinkedListValue(time_=time_, key=key))
36+
expected_node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=time_, key=key))
3737
with (
3838
patch.object(TTLMap, "_pop_ll_node") as mock_pop_ll_node,
3939
patch.object(

tests/test_update_by_ttl.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def test_update_by_ttl__empty_dict():
1515
def test_update_by_ttl__last_item():
1616
ttl = timedelta(seconds=100)
1717
d = TTLMap(ttl=ttl)
18-
time_ = time.time() + ttl.total_seconds() + 1
18+
time_ = time.monotonic() + ttl.total_seconds() + 1
1919
d._update_by_ttl(current_time=time_)
2020
assert d._ll_head is None
2121
assert d._ll_end is None
@@ -31,11 +31,11 @@ def test_update_by_ttl__expired_head():
3131
v2 = 2
3232
set_time_1 = 1
3333
set_time_2 = set_time_1 + ttl.total_seconds() + 1
34-
with patch("time.time", side_effect=[set_time_1, set_time_2]):
34+
with patch("time.monotonic", side_effect=[set_time_1, set_time_2]):
3535
d[k1] = v1
3636
d[k2] = v2
3737
d._update_by_ttl(current_time=set_time_2)
38-
node_2 = DoubleLinkedListNode(value=_LinkedListValue(time_=set_time_2, key=k2))
38+
node_2 = DoubleLinkedListNode(value=_LinkedListValue(expire_at=set_time_2 + ttl.total_seconds(), key=k2))
3939
assert d._ll_head == node_2
4040
assert d._ll_end == node_2
4141
assert d._dict == {k2: _DictValue(node=node_2, value=v2)}

ttlru_map/_ttl_map.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919

2020
@dataclass(frozen=True)
2121
class _LinkedListValue(Generic[_TKey]):
22-
__slots__ = ("key", "time_")
22+
__slots__ = ("expire_at", "key")
2323

24-
time_: float
24+
expire_at: float
2525
key: _TKey
2626

2727
def __repr__(self) -> str: # pragma: no cover
28-
return f"{self.__class__.__name__}(time_={self.time_}, key={self.key})"
28+
return f"{self.__class__.__name__}(expire_at={self.expire_at}, key={self.key})"
2929

3030

3131
@dataclass(frozen=True)
@@ -69,7 +69,7 @@ def __init__(
6969
self._ll_head: DoubleLinkedListNode[_LinkedListValue[_TKey]] | None = None
7070
self._ll_end: DoubleLinkedListNode[_LinkedListValue[_TKey]] | None = None
7171
self._max_size = max_size
72-
self._ttl = ttl
72+
self._ttl = ttl.total_seconds() if ttl is not None else None
7373
self._update_ttl_on_get = update_ttl_on_get
7474
self._lock = Lock()
7575

@@ -97,9 +97,9 @@ def _update_by_ttl(self, current_time: float | None = None) -> None:
9797
"""Remove items that have expired."""
9898
if self._ttl is None:
9999
return
100-
current_time = current_time if current_time is not None else time.time()
100+
current_time = current_time if current_time is not None else time.monotonic()
101101
while self._ll_head is not None:
102-
if self._ll_head.value.time_ + self._ttl.total_seconds() >= current_time:
102+
if self._ll_head.value.expire_at >= current_time:
103103
break
104104
del self._dict[self._ll_head.value.key]
105105
self._pop_ll_node(self._ll_head)
@@ -136,9 +136,9 @@ def _put_node_to_end(self, node: DoubleLinkedListNode[_LinkedListValue[_TKey]])
136136
node.prev = self._ll_end
137137
self._ll_end = node
138138

139-
def _setitem(self, __key: _TKey, __value: _TValue, time_: float, /) -> None:
139+
def _setitem(self, __key: _TKey, __value: _TValue, expire_at: float | None, /) -> None:
140140
"""Set an item in the dictionary and put it to the end of the linked list."""
141-
new_node = DoubleLinkedListNode(value=_LinkedListValue(time_=time_, key=__key))
141+
new_node = DoubleLinkedListNode(value=_LinkedListValue(expire_at=expire_at, key=__key))
142142

143143
if (item := self._dict.get(__key, None)) is not None:
144144
self._pop_ll_node(item.node)
@@ -155,9 +155,10 @@ def _delitem(self, item: _DictValue[_TKey, _TValue]) -> None:
155155

156156
def __setitem__(self, __key: _TKey, __value: _TValue, /) -> None:
157157
with self._lock:
158-
time_ = time.time()
159-
self._setitem(__key, __value, time_)
160-
self._update_by_ttl(current_time=time_)
158+
current_time = time.monotonic()
159+
expire_at = current_time + self._ttl if self._ttl is not None else None
160+
self._setitem(__key, __value, expire_at)
161+
self._update_by_ttl(current_time=current_time)
161162
self._update_by_size()
162163

163164
def __delitem__(self, __key: _TKey, /) -> None:
@@ -168,11 +169,11 @@ def __delitem__(self, __key: _TKey, /) -> None:
168169

169170
def __getitem__(self, __key: _TKey, /) -> _TValue:
170171
with self._lock:
171-
time_ = time.time()
172-
self._update_by_ttl(current_time=time_)
172+
current_time = time.monotonic()
173+
self._update_by_ttl(current_time=current_time)
173174
item = self._dict[__key].value
174-
if self._update_ttl_on_get:
175-
self._setitem(__key, item, time_)
175+
if self._update_ttl_on_get and self._ttl is not None:
176+
self._setitem(__key, item, current_time + self._ttl)
176177
return item
177178

178179
def __len__(self) -> int:

0 commit comments

Comments
 (0)