From 2bb7f102f6b6724f5d5d9f2cdb187226ab5d2167 Mon Sep 17 00:00:00 2001 From: Brad MacPhee Date: Tue, 20 Jun 2023 08:41:53 -0300 Subject: [PATCH] Introduce OutOfMemoryError exception for Redis write command rejections due to OOM errors (#2778) * expose OutOfMemoryError as explicit exception type - handle "OOM" error code string by raising explicit exception type instance - enables callers to avoid string matching after catching ResponseError * add OutOfMemoryError exception class docstring * Provide more info in the exception docstring * Fix formatting * Again * linters --------- Co-authored-by: Chayim Co-authored-by: Igor Malinovskiy Co-authored-by: dvora-h <67596500+dvora-h@users.noreply.github.com> --- redis/__init__.py | 2 ++ redis/asyncio/__init__.py | 2 ++ redis/asyncio/connection.py | 2 ++ redis/connection.py | 2 ++ redis/exceptions.py | 12 ++++++++++++ tests/test_asyncio/test_connection_pool.py | 10 +++++++++- tests/test_connection_pool.py | 9 ++++++++- 7 files changed, 37 insertions(+), 2 deletions(-) diff --git a/redis/__init__.py b/redis/__init__.py index b8850add15..d7b74edf41 100644 --- a/redis/__init__.py +++ b/redis/__init__.py @@ -19,6 +19,7 @@ ConnectionError, DataError, InvalidResponse, + OutOfMemoryError, PubSubError, ReadOnlyError, RedisError, @@ -72,6 +73,7 @@ def int_or_str(value): "from_url", "default_backoff", "InvalidResponse", + "OutOfMemoryError", "PubSubError", "ReadOnlyError", "Redis", diff --git a/redis/asyncio/__init__.py b/redis/asyncio/__init__.py index bf90dde555..2a82df251e 100644 --- a/redis/asyncio/__init__.py +++ b/redis/asyncio/__init__.py @@ -24,6 +24,7 @@ ConnectionError, DataError, InvalidResponse, + OutOfMemoryError, PubSubError, ReadOnlyError, RedisError, @@ -47,6 +48,7 @@ "default_backoff", "InvalidResponse", "PubSubError", + "OutOfMemoryError", "ReadOnlyError", "Redis", "RedisCluster", diff --git a/redis/asyncio/connection.py b/redis/asyncio/connection.py index 4ba177ef26..efe3a3e1b0 100644 --- a/redis/asyncio/connection.py +++ b/redis/asyncio/connection.py @@ -49,6 +49,7 @@ ModuleError, NoPermissionError, NoScriptError, + OutOfMemoryError, ReadOnlyError, RedisError, ResponseError, @@ -174,6 +175,7 @@ class BaseParser: "READONLY": ReadOnlyError, "NOAUTH": AuthenticationError, "NOPERM": NoPermissionError, + "OOM": OutOfMemoryError, } def __init__(self, socket_read_size: int): diff --git a/redis/connection.py b/redis/connection.py index 5af8928a5d..bf0d6dea80 100644 --- a/redis/connection.py +++ b/redis/connection.py @@ -28,6 +28,7 @@ ModuleError, NoPermissionError, NoScriptError, + OutOfMemoryError, ReadOnlyError, RedisError, ResponseError, @@ -149,6 +150,7 @@ class BaseParser: MODULE_UNLOAD_NOT_POSSIBLE_ERROR: ModuleError, **NO_AUTH_SET_ERROR, }, + "OOM": OutOfMemoryError, "WRONGPASS": AuthenticationError, "EXECABORT": ExecAbortError, "LOADING": BusyLoadingError, diff --git a/redis/exceptions.py b/redis/exceptions.py index 7c705d3c15..7cf15a7d07 100644 --- a/redis/exceptions.py +++ b/redis/exceptions.py @@ -49,6 +49,18 @@ class NoScriptError(ResponseError): pass +class OutOfMemoryError(ResponseError): + """ + Indicates the database is full. Can only occur when either: + * Redis maxmemory-policy=noeviction + * Redis maxmemory-policy=volatile* and there are no evictable keys + + For more information see `Memory optimization in Redis `_. # noqa + """ + + pass + + class ExecAbortError(ResponseError): pass diff --git a/tests/test_asyncio/test_connection_pool.py b/tests/test_asyncio/test_connection_pool.py index 92499e2c4a..24d9902142 100644 --- a/tests/test_asyncio/test_connection_pool.py +++ b/tests/test_asyncio/test_connection_pool.py @@ -606,10 +606,18 @@ async def test_busy_loading_from_pipeline(self, r): @skip_if_server_version_lt("2.8.8") @skip_if_redis_enterprise() async def test_read_only_error(self, r): - """READONLY errors get turned in ReadOnlyError exceptions""" + """READONLY errors get turned into ReadOnlyError exceptions""" with pytest.raises(redis.ReadOnlyError): await r.execute_command("DEBUG", "ERROR", "READONLY blah blah") + @skip_if_redis_enterprise() + async def test_oom_error(self, r): + """OOM errors get turned into OutOfMemoryError exceptions""" + with pytest.raises(redis.OutOfMemoryError): + # note: don't use the DEBUG OOM command since it's not the same + # as the db being full + await r.execute_command("DEBUG", "ERROR", "OOM blah blah") + def test_connect_from_url_tcp(self): connection = redis.Redis.from_url("redis://localhost") pool = connection.connection_pool diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index e8a42692a1..155bffe56a 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -528,10 +528,17 @@ def test_busy_loading_from_pipeline(self, r): @skip_if_server_version_lt("2.8.8") @skip_if_redis_enterprise() def test_read_only_error(self, r): - "READONLY errors get turned in ReadOnlyError exceptions" + "READONLY errors get turned into ReadOnlyError exceptions" with pytest.raises(redis.ReadOnlyError): r.execute_command("DEBUG", "ERROR", "READONLY blah blah") + def test_oom_error(self, r): + "OOM errors get turned into OutOfMemoryError exceptions" + with pytest.raises(redis.OutOfMemoryError): + # note: don't use the DEBUG OOM command since it's not the same + # as the db being full + r.execute_command("DEBUG", "ERROR", "OOM blah blah") + def test_connect_from_url_tcp(self): connection = redis.Redis.from_url("redis://localhost") pool = connection.connection_pool