Skip to content

Commit 0934a79

Browse files
authored
Merge pull request #61 from adarshprabhu03/fix-timeline-ttl-expiry
Fix: timeline ttl expiry
2 parents 565616c + 9f6ebfa commit 0934a79

2 files changed

Lines changed: 85 additions & 0 deletions

File tree

packages/langgraph-checkpoint-aerospike/langgraph/checkpoint/aerospike/saver.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,10 @@ def get_tuple(
330330
if got is None:
331331
return None
332332

333+
# Refresh timeline record TTL if refresh_on_read is configured
334+
if self._refresh_on_read and self._ttl_minutes is not None and self._ttl_minutes > 0:
335+
self._get(self._key_timeline(thread_id, checkpoint_ns))
336+
333337
_, _, bins = got
334338

335339
cp_type = bins.get("cp_type")

packages/langgraph-checkpoint-aerospike/tests/test_ttl_behavior.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,84 @@ def test_ttl_resets_on_read(short_ttl_saver, client, aerospike_namespace):
9999
# ttl_after would be roughly ttl_before - 1 or less.
100100
# Allowing equality handles coarse timer resolution.
101101
assert ttl_after >= ttl_before, f"Expected ttl_after ({ttl_after}) >= ttl_before ({ttl_before})"
102+
103+
104+
def test_timeline_refreshes_on_get(short_ttl_saver, client):
105+
"""Verify that calling get_tuple refreshes the timeline record's TTL."""
106+
cfg = {
107+
"configurable": {
108+
"thread_id": "timeline-refresh-demo",
109+
"checkpoint_ns": "demo-ns",
110+
}
111+
}
112+
checkpoint = {"id": "ck-timeline-1", "foo": "bar"}
113+
metadata = {}
114+
115+
saved_config = short_ttl_saver.put(cfg, checkpoint, metadata, {})
116+
117+
timeline_key = short_ttl_saver._key_timeline(
118+
cfg["configurable"]["thread_id"], cfg["configurable"]["checkpoint_ns"]
119+
)
120+
121+
# Wait to let TTL decrease
122+
time.sleep(10)
123+
124+
_, meta_before, _ = client.get(timeline_key)
125+
ttl_before = meta_before["ttl"]
126+
127+
# Call get_tuple to trigger refresh
128+
short_ttl_saver.get_tuple(saved_config)
129+
130+
# Wait a bit to let policy touch and register
131+
time.sleep(1)
132+
133+
_, meta_after, _ = client.get(timeline_key)
134+
ttl_after = meta_after["ttl"]
135+
136+
assert ttl_after >= ttl_before, (
137+
f"Expected timeline TTL to be refreshed, got {ttl_after} (was {ttl_before})"
138+
)
139+
140+
141+
def test_timeline_refreshes_on_get_mocked():
142+
"""Verify via mocking that get_tuple refreshes the timeline record's TTL when refresh_on_read is True."""
143+
from unittest.mock import MagicMock
144+
145+
from langgraph.checkpoint.aerospike import AerospikeSaver
146+
147+
# Create mock client
148+
mock_client = MagicMock()
149+
150+
# Aerospike client.get returns a tuple: (key, meta, bins)
151+
# where bins contains the checkpoint data
152+
checkpoint_bins = {
153+
"cp_type": "json",
154+
"checkpoint": b'{"id": "ck-1", "ts": "2026-06-26T00:00:00Z"}',
155+
"meta_type": "json",
156+
"metadata": b"{}",
157+
}
158+
mock_client.get.side_effect = lambda key, policy=None: (key, {"ttl": 60}, checkpoint_bins)
159+
160+
saver = AerospikeSaver(
161+
client=mock_client, namespace="test", ttl={"default_ttl": 60, "refresh_on_read": True}
162+
)
163+
164+
# Call get_tuple
165+
config = {
166+
"configurable": {"thread_id": "thread-1", "checkpoint_ns": "ns-1", "checkpoint_id": "ck-1"}
167+
}
168+
169+
saver.get_tuple(config)
170+
171+
# Verify that client.get was called for the timeline key with the read_touch policy
172+
timeline_key = saver._key_timeline("thread-1", "ns-1")
173+
174+
# Let's inspect all calls to client.get
175+
get_calls = mock_client.get.call_args_list
176+
177+
# We expect get_calls to contain the timeline key read
178+
timeline_call = next((call for call in get_calls if call[0][0] == timeline_key), None)
179+
180+
assert timeline_call is not None, "Expected client.get to be called on timeline record key"
181+
# The policy should have read_touch_ttl_percent = 100
182+
assert timeline_call[1].get("policy") == {"read_touch_ttl_percent": 100}

0 commit comments

Comments
 (0)