Skip to content

Commit 2fbff83

Browse files
aiven-salgerzse
andcommitted
Hash field expiration commands (redis#3218)
Support hash field expiration commands that become available with Redis 7.4. Adapt some tests to match recent server-side changes. Update tests related to memory stats. Make CLIENT KILL test not run with cluster. --------- Co-authored-by: Gabriel Erzse <[email protected]> Signed-off-by: Salvatore Mesoraca <[email protected]>
1 parent 06ab28b commit 2fbff83

File tree

7 files changed

+1045
-7
lines changed

7 files changed

+1045
-7
lines changed

tests/test_asyncio/test_cluster.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1430,7 +1430,7 @@ async def test_memory_stats(self, r: ValkeyCluster) -> None:
14301430
assert isinstance(stats, dict)
14311431
for key, value in stats.items():
14321432
if key.startswith("db."):
1433-
assert isinstance(value, dict)
1433+
assert not isinstance(value, list)
14341434

14351435
@skip_if_server_version_lt("4.0.0")
14361436
async def test_memory_help(self, r: ValkeyCluster) -> None:

tests/test_asyncio/test_commands.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1352,7 +1352,7 @@ async def test_hscan(self, r: valkey.Valkey):
13521352
_, dic = await r.hscan("a_notset", match="a")
13531353
assert dic == {}
13541354

1355-
@skip_if_server_version_lt("7.4.0")
1355+
@skip_if_server_version_lt("7.3.240")
13561356
async def test_hscan_novalues(self, r: valkey.Valkey):
13571357
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
13581358
cursor, keys = await r.hscan("a", no_values=True)
@@ -1373,7 +1373,7 @@ async def test_hscan_iter(self, r: valkey.Valkey):
13731373
dic = {k: v async for k, v in r.hscan_iter("a_notset", match="a")}
13741374
assert dic == {}
13751375

1376-
@skip_if_server_version_lt("7.4.0")
1376+
@skip_if_server_version_lt("7.3.240")
13771377
async def test_hscan_iter_novalues(self, r: valkey.Valkey):
13781378
await r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
13791379
keys = list([k async for k in r.hscan_iter("a", no_values=True)])

tests/test_asyncio/test_hash.py

+300
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import asyncio
2+
from datetime import datetime, timedelta
3+
4+
from tests.conftest import skip_if_server_version_lt
5+
6+
7+
@skip_if_server_version_lt("7.3.240")
8+
async def test_hexpire_basic(r):
9+
await r.delete("test:hash")
10+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
11+
assert await r.hexpire("test:hash", 1, "field1") == [1]
12+
await asyncio.sleep(1.1)
13+
assert await r.hexists("test:hash", "field1") is False
14+
assert await r.hexists("test:hash", "field2") is True
15+
16+
17+
@skip_if_server_version_lt("7.3.240")
18+
async def test_hexpire_with_timedelta(r):
19+
await r.delete("test:hash")
20+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
21+
assert await r.hexpire("test:hash", timedelta(seconds=1), "field1") == [1]
22+
await asyncio.sleep(1.1)
23+
assert await r.hexists("test:hash", "field1") is False
24+
assert await r.hexists("test:hash", "field2") is True
25+
26+
27+
@skip_if_server_version_lt("7.3.240")
28+
async def test_hexpire_conditions(r):
29+
await r.delete("test:hash")
30+
await r.hset("test:hash", mapping={"field1": "value1"})
31+
assert await r.hexpire("test:hash", 2, "field1", xx=True) == [0]
32+
assert await r.hexpire("test:hash", 2, "field1", nx=True) == [1]
33+
assert await r.hexpire("test:hash", 1, "field1", xx=True) == [1]
34+
assert await r.hexpire("test:hash", 2, "field1", nx=True) == [0]
35+
await asyncio.sleep(1.1)
36+
assert await r.hexists("test:hash", "field1") is False
37+
await r.hset("test:hash", "field1", "value1")
38+
await r.hexpire("test:hash", 2, "field1")
39+
assert await r.hexpire("test:hash", 1, "field1", gt=True) == [0]
40+
assert await r.hexpire("test:hash", 1, "field1", lt=True) == [1]
41+
await asyncio.sleep(1.1)
42+
assert await r.hexists("test:hash", "field1") is False
43+
44+
45+
@skip_if_server_version_lt("7.3.240")
46+
async def test_hexpire_nonexistent_key_or_field(r):
47+
await r.delete("test:hash")
48+
assert await r.hexpire("test:hash", 1, "field1") == []
49+
await r.hset("test:hash", "field1", "value1")
50+
assert await r.hexpire("test:hash", 1, "nonexistent_field") == [-2]
51+
52+
53+
@skip_if_server_version_lt("7.3.240")
54+
async def test_hexpire_multiple_fields(r):
55+
await r.delete("test:hash")
56+
await r.hset(
57+
"test:hash",
58+
mapping={"field1": "value1", "field2": "value2", "field3": "value3"},
59+
)
60+
assert await r.hexpire("test:hash", 1, "field1", "field2") == [1, 1]
61+
await asyncio.sleep(1.1)
62+
assert await r.hexists("test:hash", "field1") is False
63+
assert await r.hexists("test:hash", "field2") is False
64+
assert await r.hexists("test:hash", "field3") is True
65+
66+
67+
@skip_if_server_version_lt("7.3.240")
68+
async def test_hpexpire_basic(r):
69+
await r.delete("test:hash")
70+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
71+
assert await r.hpexpire("test:hash", 500, "field1") == [1]
72+
await asyncio.sleep(0.6)
73+
assert await r.hexists("test:hash", "field1") is False
74+
assert await r.hexists("test:hash", "field2") is True
75+
76+
77+
@skip_if_server_version_lt("7.3.240")
78+
async def test_hpexpire_with_timedelta(r):
79+
await r.delete("test:hash")
80+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
81+
assert await r.hpexpire("test:hash", timedelta(milliseconds=500), "field1") == [1]
82+
await asyncio.sleep(0.6)
83+
assert await r.hexists("test:hash", "field1") is False
84+
assert await r.hexists("test:hash", "field2") is True
85+
86+
87+
@skip_if_server_version_lt("7.3.240")
88+
async def test_hpexpire_conditions(r):
89+
await r.delete("test:hash")
90+
await r.hset("test:hash", mapping={"field1": "value1"})
91+
assert await r.hpexpire("test:hash", 1500, "field1", xx=True) == [0]
92+
assert await r.hpexpire("test:hash", 1500, "field1", nx=True) == [1]
93+
assert await r.hpexpire("test:hash", 500, "field1", xx=True) == [1]
94+
assert await r.hpexpire("test:hash", 1500, "field1", nx=True) == [0]
95+
await asyncio.sleep(0.6)
96+
assert await r.hexists("test:hash", "field1") is False
97+
await r.hset("test:hash", "field1", "value1")
98+
await r.hpexpire("test:hash", 1000, "field1")
99+
assert await r.hpexpire("test:hash", 500, "field1", gt=True) == [0]
100+
assert await r.hpexpire("test:hash", 500, "field1", lt=True) == [1]
101+
await asyncio.sleep(0.6)
102+
assert await r.hexists("test:hash", "field1") is False
103+
104+
105+
@skip_if_server_version_lt("7.3.240")
106+
async def test_hpexpire_nonexistent_key_or_field(r):
107+
await r.delete("test:hash")
108+
assert await r.hpexpire("test:hash", 500, "field1") == []
109+
await r.hset("test:hash", "field1", "value1")
110+
assert await r.hpexpire("test:hash", 500, "nonexistent_field") == [-2]
111+
112+
113+
@skip_if_server_version_lt("7.3.240")
114+
async def test_hpexpire_multiple_fields(r):
115+
await r.delete("test:hash")
116+
await r.hset(
117+
"test:hash",
118+
mapping={"field1": "value1", "field2": "value2", "field3": "value3"},
119+
)
120+
assert await r.hpexpire("test:hash", 500, "field1", "field2") == [1, 1]
121+
await asyncio.sleep(0.6)
122+
assert await r.hexists("test:hash", "field1") is False
123+
assert await r.hexists("test:hash", "field2") is False
124+
assert await r.hexists("test:hash", "field3") is True
125+
126+
127+
@skip_if_server_version_lt("7.3.240")
128+
async def test_hexpireat_basic(r):
129+
await r.delete("test:hash")
130+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
131+
exp_time = int((datetime.now() + timedelta(seconds=1)).timestamp())
132+
assert await r.hexpireat("test:hash", exp_time, "field1") == [1]
133+
await asyncio.sleep(1.1)
134+
assert await r.hexists("test:hash", "field1") is False
135+
assert await r.hexists("test:hash", "field2") is True
136+
137+
138+
@skip_if_server_version_lt("7.3.240")
139+
async def test_hexpireat_with_datetime(r):
140+
await r.delete("test:hash")
141+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
142+
exp_time = datetime.now() + timedelta(seconds=1)
143+
assert await r.hexpireat("test:hash", exp_time, "field1") == [1]
144+
await asyncio.sleep(1.1)
145+
assert await r.hexists("test:hash", "field1") is False
146+
assert await r.hexists("test:hash", "field2") is True
147+
148+
149+
@skip_if_server_version_lt("7.3.240")
150+
async def test_hexpireat_conditions(r):
151+
await r.delete("test:hash")
152+
await r.hset("test:hash", mapping={"field1": "value1"})
153+
future_exp_time = int((datetime.now() + timedelta(seconds=2)).timestamp())
154+
past_exp_time = int((datetime.now() - timedelta(seconds=1)).timestamp())
155+
assert await r.hexpireat("test:hash", future_exp_time, "field1", xx=True) == [0]
156+
assert await r.hexpireat("test:hash", future_exp_time, "field1", nx=True) == [1]
157+
assert await r.hexpireat("test:hash", past_exp_time, "field1", gt=True) == [0]
158+
assert await r.hexpireat("test:hash", past_exp_time, "field1", lt=True) == [2]
159+
assert await r.hexists("test:hash", "field1") is False
160+
161+
162+
@skip_if_server_version_lt("7.3.240")
163+
async def test_hexpireat_nonexistent_key_or_field(r):
164+
await r.delete("test:hash")
165+
future_exp_time = int((datetime.now() + timedelta(seconds=1)).timestamp())
166+
assert await r.hexpireat("test:hash", future_exp_time, "field1") == []
167+
await r.hset("test:hash", "field1", "value1")
168+
assert await r.hexpireat("test:hash", future_exp_time, "nonexistent_field") == [-2]
169+
170+
171+
@skip_if_server_version_lt("7.3.240")
172+
async def test_hexpireat_multiple_fields(r):
173+
await r.delete("test:hash")
174+
await r.hset(
175+
"test:hash",
176+
mapping={"field1": "value1", "field2": "value2", "field3": "value3"},
177+
)
178+
exp_time = int((datetime.now() + timedelta(seconds=1)).timestamp())
179+
assert await r.hexpireat("test:hash", exp_time, "field1", "field2") == [1, 1]
180+
await asyncio.sleep(1.1)
181+
assert await r.hexists("test:hash", "field1") is False
182+
assert await r.hexists("test:hash", "field2") is False
183+
assert await r.hexists("test:hash", "field3") is True
184+
185+
186+
@skip_if_server_version_lt("7.3.240")
187+
async def test_hpexpireat_basic(r):
188+
await r.delete("test:hash")
189+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
190+
exp_time = int((datetime.now() + timedelta(milliseconds=400)).timestamp() * 1000)
191+
assert await r.hpexpireat("test:hash", exp_time, "field1") == [1]
192+
await asyncio.sleep(0.5)
193+
assert await r.hexists("test:hash", "field1") is False
194+
assert await r.hexists("test:hash", "field2") is True
195+
196+
197+
@skip_if_server_version_lt("7.3.240")
198+
async def test_hpexpireat_with_datetime(r):
199+
await r.delete("test:hash")
200+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
201+
exp_time = datetime.now() + timedelta(milliseconds=400)
202+
assert await r.hpexpireat("test:hash", exp_time, "field1") == [1]
203+
await asyncio.sleep(0.5)
204+
assert await r.hexists("test:hash", "field1") is False
205+
assert await r.hexists("test:hash", "field2") is True
206+
207+
208+
@skip_if_server_version_lt("7.3.240")
209+
async def test_hpexpireat_conditions(r):
210+
await r.delete("test:hash")
211+
await r.hset("test:hash", mapping={"field1": "value1"})
212+
future_exp_time = int(
213+
(datetime.now() + timedelta(milliseconds=500)).timestamp() * 1000
214+
)
215+
past_exp_time = int(
216+
(datetime.now() - timedelta(milliseconds=500)).timestamp() * 1000
217+
)
218+
assert await r.hpexpireat("test:hash", future_exp_time, "field1", xx=True) == [0]
219+
assert await r.hpexpireat("test:hash", future_exp_time, "field1", nx=True) == [1]
220+
assert await r.hpexpireat("test:hash", past_exp_time, "field1", gt=True) == [0]
221+
assert await r.hpexpireat("test:hash", past_exp_time, "field1", lt=True) == [2]
222+
assert await r.hexists("test:hash", "field1") is False
223+
224+
225+
@skip_if_server_version_lt("7.3.240")
226+
async def test_hpexpireat_nonexistent_key_or_field(r):
227+
await r.delete("test:hash")
228+
future_exp_time = int(
229+
(datetime.now() + timedelta(milliseconds=500)).timestamp() * 1000
230+
)
231+
assert await r.hpexpireat("test:hash", future_exp_time, "field1") == []
232+
await r.hset("test:hash", "field1", "value1")
233+
assert await r.hpexpireat("test:hash", future_exp_time, "nonexistent_field") == [-2]
234+
235+
236+
@skip_if_server_version_lt("7.3.240")
237+
async def test_hpexpireat_multiple_fields(r):
238+
await r.delete("test:hash")
239+
await r.hset(
240+
"test:hash",
241+
mapping={"field1": "value1", "field2": "value2", "field3": "value3"},
242+
)
243+
exp_time = int((datetime.now() + timedelta(milliseconds=400)).timestamp() * 1000)
244+
assert await r.hpexpireat("test:hash", exp_time, "field1", "field2") == [1, 1]
245+
await asyncio.sleep(0.5)
246+
assert await r.hexists("test:hash", "field1") is False
247+
assert await r.hexists("test:hash", "field2") is False
248+
assert await r.hexists("test:hash", "field3") is True
249+
250+
251+
@skip_if_server_version_lt("7.3.240")
252+
async def test_hpersist_multiple_fields_mixed_conditions(r):
253+
await r.delete("test:hash")
254+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
255+
await r.hexpire("test:hash", 5000, "field1")
256+
assert await r.hpersist("test:hash", "field1", "field2", "field3") == [1, -1, -2]
257+
258+
259+
@skip_if_server_version_lt("7.3.240")
260+
async def test_hexpiretime_multiple_fields_mixed_conditions(r):
261+
await r.delete("test:hash")
262+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
263+
future_time = int((datetime.now() + timedelta(minutes=30)).timestamp())
264+
await r.hexpireat("test:hash", future_time, "field1")
265+
result = await r.hexpiretime("test:hash", "field1", "field2", "field3")
266+
assert future_time - 10 < result[0] <= future_time
267+
assert result[1:] == [-1, -2]
268+
269+
270+
@skip_if_server_version_lt("7.3.240")
271+
async def test_hpexpiretime_multiple_fields_mixed_conditions(r):
272+
await r.delete("test:hash")
273+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
274+
future_time = int((datetime.now() + timedelta(minutes=30)).timestamp())
275+
await r.hexpireat("test:hash", future_time, "field1")
276+
result = await r.hpexpiretime("test:hash", "field1", "field2", "field3")
277+
assert future_time * 1000 - 10000 < result[0] <= future_time * 1000
278+
assert result[1:] == [-1, -2]
279+
280+
281+
@skip_if_server_version_lt("7.3.240")
282+
async def test_ttl_multiple_fields_mixed_conditions(r):
283+
await r.delete("test:hash")
284+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
285+
future_time = int((datetime.now() + timedelta(minutes=30)).timestamp())
286+
await r.hexpireat("test:hash", future_time, "field1")
287+
result = await r.httl("test:hash", "field1", "field2", "field3")
288+
assert 30 * 60 - 10 < result[0] <= 30 * 60
289+
assert result[1:] == [-1, -2]
290+
291+
292+
@skip_if_server_version_lt("7.3.240")
293+
async def test_pttl_multiple_fields_mixed_conditions(r):
294+
await r.delete("test:hash")
295+
await r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"})
296+
future_time = int((datetime.now() + timedelta(minutes=30)).timestamp())
297+
await r.hexpireat("test:hash", future_time, "field1")
298+
result = await r.hpttl("test:hash", "field1", "field2", "field3")
299+
assert 30 * 60000 - 10000 < result[0] <= 30 * 60000
300+
assert result[1:] == [-1, -2]

tests/test_cluster.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1548,7 +1548,7 @@ def test_memory_stats(self, r):
15481548
assert isinstance(stats, dict)
15491549
for key, value in stats.items():
15501550
if key.startswith("db."):
1551-
assert isinstance(value, dict)
1551+
assert not isinstance(value, list)
15521552

15531553
@skip_if_server_version_lt("4.0.0")
15541554
def test_memory_help(self, r):

tests/test_commands.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,8 @@ def test_client_kill_filter_by_user(self, r, request):
692692
assert c["user"] != killuser
693693
r.acl_deluser(killuser)
694694

695-
@skip_if_server_version_lt("7.4.0")
695+
@skip_if_server_version_lt("7.3.240")
696+
@pytest.mark.onlynoncluster
696697
def test_client_kill_filter_by_maxage(self, r, request):
697698
_get_client(valkey.Valkey, request, flushdb=False)
698699
time.sleep(4)
@@ -2133,7 +2134,7 @@ def test_hscan(self, r):
21332134
_, dic = r.hscan("a_notset")
21342135
assert dic == {}
21352136

2136-
@skip_if_server_version_lt("7.4.0")
2137+
@skip_if_server_version_lt("7.3.240")
21372138
def test_hscan_novalues(self, r):
21382139
r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
21392140
cursor, keys = r.hscan("a", no_values=True)
@@ -2154,7 +2155,7 @@ def test_hscan_iter(self, r):
21542155
dic = dict(r.hscan_iter("a_notset"))
21552156
assert dic == {}
21562157

2157-
@skip_if_server_version_lt("7.4.0")
2158+
@skip_if_server_version_lt("7.3.240")
21582159
def test_hscan_iter_novalues(self, r):
21592160
r.hset("a", mapping={"a": 1, "b": 2, "c": 3})
21602161
keys = list(r.hscan_iter("a", no_values=True))

0 commit comments

Comments
 (0)