diff --git a/README.rst b/README.rst index 266780c4..ffa3d482 100644 --- a/README.rst +++ b/README.rst @@ -483,6 +483,15 @@ pattern syntax as the ``keys`` function and returns the number of deleted keys. >>> from django.core.cache import cache >>> cache.delete_pattern("foo_*") +To achieve the best performance while deleting many keys, you should set ``DJANGO_REDIS_SCAN_ITERSIZE`` to a relatively +high number (e.g., 100_000) by default in Django settings or pass it directly to the ``delete_pattern``. + + +.. code-block:: pycon + + >>> from django.core.cache import cache + >>> cache.delete_pattern("foo_*", itersize=100_000) + Redis native commands ~~~~~~~~~~~~~~~~~~~~~ diff --git a/django_redis/client/default.py b/django_redis/client/default.py index 1df90a27..6886b46b 100644 --- a/django_redis/client/default.py +++ b/django_redis/client/default.py @@ -393,9 +393,13 @@ def delete_pattern( try: count = 0 + pipeline = client.pipeline() + for key in client.scan_iter(match=pattern, count=itersize): - client.delete(key) + pipeline.delete(key) count += 1 + pipeline.execute() + return count except _main_exceptions as e: raise ConnectionInterrupted(connection=client) from e diff --git a/tests/test_client.py b/tests/test_client.py index 807fbf3a..74308657 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,5 +1,5 @@ from typing import Iterable -from unittest.mock import Mock, patch +from unittest.mock import Mock, call, patch import pytest from django.core.cache import DEFAULT_CACHE_ALIAS @@ -104,6 +104,28 @@ def test_delete_pattern_calls_scan_iter_with_count_if_itersize_given( count=90210, match=make_pattern_mock.return_value ) + @patch("test_client.DefaultClient.make_pattern") + @patch("test_client.DefaultClient.get_client", return_value=Mock()) + @patch("test_client.DefaultClient.__init__", return_value=None) + def test_delete_pattern_calls_pipeline_delete_and_execute( + self, init_mock, get_client_mock, make_pattern_mock + ): + client = DefaultClient() + client._backend = Mock() + client._backend.key_prefix = "" + get_client_mock.return_value.scan_iter.return_value = [":1:foo", ":1:foo-a"] + get_client_mock.return_value.pipeline.return_value = Mock() + get_client_mock.return_value.pipeline.return_value.delete = Mock() + get_client_mock.return_value.pipeline.return_value.execute = Mock() + + client.delete_pattern(pattern="foo*") + + assert get_client_mock.return_value.pipeline.return_value.delete.call_count == 2 + get_client_mock.return_value.pipeline.return_value.delete.assert_has_calls( + [call(":1:foo"), call(":1:foo-a")] + ) + get_client_mock.return_value.pipeline.return_value.execute.assert_called_once() + class TestShardClient: @patch("test_client.DefaultClient.make_pattern")