Skip to content

Commit

Permalink
Introduce OutOfMemoryError exception for Redis write command rejectio…
Browse files Browse the repository at this point in the history
…ns 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 <chayim@users.noreply.github.com>
Co-authored-by: Igor Malinovskiy <u.glide@gmail.com>
Co-authored-by: dvora-h <67596500+dvora-h@users.noreply.github.com>
  • Loading branch information
4 people authored Jun 20, 2023
1 parent 29dfbb2 commit 2bb7f10
Show file tree
Hide file tree
Showing 7 changed files with 37 additions and 2 deletions.
2 changes: 2 additions & 0 deletions redis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ConnectionError,
DataError,
InvalidResponse,
OutOfMemoryError,
PubSubError,
ReadOnlyError,
RedisError,
Expand Down Expand Up @@ -72,6 +73,7 @@ def int_or_str(value):
"from_url",
"default_backoff",
"InvalidResponse",
"OutOfMemoryError",
"PubSubError",
"ReadOnlyError",
"Redis",
Expand Down
2 changes: 2 additions & 0 deletions redis/asyncio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
ConnectionError,
DataError,
InvalidResponse,
OutOfMemoryError,
PubSubError,
ReadOnlyError,
RedisError,
Expand All @@ -47,6 +48,7 @@
"default_backoff",
"InvalidResponse",
"PubSubError",
"OutOfMemoryError",
"ReadOnlyError",
"Redis",
"RedisCluster",
Expand Down
2 changes: 2 additions & 0 deletions redis/asyncio/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
ModuleError,
NoPermissionError,
NoScriptError,
OutOfMemoryError,
ReadOnlyError,
RedisError,
ResponseError,
Expand Down Expand Up @@ -174,6 +175,7 @@ class BaseParser:
"READONLY": ReadOnlyError,
"NOAUTH": AuthenticationError,
"NOPERM": NoPermissionError,
"OOM": OutOfMemoryError,
}

def __init__(self, socket_read_size: int):
Expand Down
2 changes: 2 additions & 0 deletions redis/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
ModuleError,
NoPermissionError,
NoScriptError,
OutOfMemoryError,
ReadOnlyError,
RedisError,
ResponseError,
Expand Down Expand Up @@ -149,6 +150,7 @@ class BaseParser:
MODULE_UNLOAD_NOT_POSSIBLE_ERROR: ModuleError,
**NO_AUTH_SET_ERROR,
},
"OOM": OutOfMemoryError,
"WRONGPASS": AuthenticationError,
"EXECABORT": ExecAbortError,
"LOADING": BusyLoadingError,
Expand Down
12 changes: 12 additions & 0 deletions redis/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://redis.io/docs/management/optimization/memory-optimization/#memory-allocation>`_. # noqa
"""

pass


class ExecAbortError(ResponseError):
pass

Expand Down
10 changes: 9 additions & 1 deletion tests/test_asyncio/test_connection_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion tests/test_connection_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 2bb7f10

Please sign in to comment.