Skip to content

Commit ae1b27d

Browse files
authored
Merge pull request #269 from benavlabs/cache-delete-pattern-scan
fix RedisBackend.delete_pattern to page the SCAN cursor so all matchi…
2 parents 1deccbb + b09ef31 commit ae1b27d

2 files changed

Lines changed: 27 additions & 9 deletions

File tree

backend/src/infrastructure/cache/backends/redis.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,19 @@ async def delete(self, key: str) -> None:
106106
async def delete_pattern(self, pattern: str) -> None:
107107
"""Delete all keys matching a pattern.
108108
109+
Pages the ``SCAN`` cursor to completion so every match is removed, not just
110+
the first batch. Each batch is deleted as it is scanned.
111+
109112
Args:
110113
pattern: The pattern to match against keys.
111114
"""
112115
cursor = 0
113-
keys_to_delete = []
114-
115-
cursor_response, keys = await self.client.scan(cursor=cursor, match=pattern + "*", count=100)
116-
117-
if keys:
118-
keys_to_delete.extend(keys)
119-
120-
if keys_to_delete:
121-
await self.client.delete(*keys_to_delete)
116+
while True:
117+
cursor, keys = await self.client.scan(cursor=cursor, match=pattern + "*", count=100)
118+
if keys:
119+
await self.client.delete(*keys)
120+
if cursor == 0:
121+
break
122122

123123
async def exists(self, key: str) -> bool:
124124
"""Check if a key exists in the cache.

backend/tests/unit/infrastructure/cache/backends/test_redis.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,21 @@ async def test_delete_pattern(redis_backend, mock_redis):
7373
mock_redis.scan.assert_called_once_with(cursor=0, match="test_*", count=100)
7474

7575
mock_redis.delete.assert_called_once_with(b"key1", b"key2", b"key3")
76+
77+
78+
@pytest.mark.asyncio
79+
async def test_delete_pattern_pages_cursor(redis_backend, mock_redis):
80+
"""delete_pattern pages the SCAN cursor to completion, not just the first batch.
81+
82+
Regression: the old implementation ran a single SCAN and silently left matches
83+
beyond the first window undeleted.
84+
"""
85+
mock_redis.scan.side_effect = [(5, [b"a1", b"a2"]), (0, [b"a3"])]
86+
87+
await redis_backend.delete_pattern("a")
88+
89+
assert mock_redis.scan.await_count == 2
90+
# the second scan resumes from the cursor the first one returned
91+
assert mock_redis.scan.await_args_list[1].kwargs["cursor"] == 5
92+
deleted = [k for call in mock_redis.delete.await_args_list for k in call.args]
93+
assert deleted == [b"a1", b"a2", b"a3"]

0 commit comments

Comments
 (0)