-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix async client safety #3512
base: master
Are you sure you want to change the base?
Fix async client safety #3512
Changes from all commits
c87e01f
95693c8
3953ac3
34ce3de
955df70
1580dd4
cf079d1
a12aff3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -362,6 +362,12 @@ def __init__( | |
# on a set of redis commands | ||
self._single_conn_lock = asyncio.Lock() | ||
|
||
# When used as an async context manager, we need to increment and decrement | ||
# a usage counter so that we can close the connection pool when no one is | ||
# using the client. | ||
self._usage_counter = 0 | ||
self._usage_lock = asyncio.Lock() | ||
|
||
def __repr__(self): | ||
return ( | ||
f"<{self.__class__.__module__}.{self.__class__.__name__}" | ||
|
@@ -562,10 +568,40 @@ def client(self) -> "Redis": | |
) | ||
|
||
async def __aenter__(self: _RedisT) -> _RedisT: | ||
return await self.initialize() | ||
""" | ||
Async context manager entry. Increments a usage counter so that the | ||
connection pool is only closed (via aclose()) when no context is using | ||
the client. | ||
""" | ||
async with self._usage_lock: | ||
self._usage_counter += 1 | ||
try: | ||
# Initialize the client (i.e. establish connection, etc.) | ||
return await self.initialize() | ||
except Exception: | ||
# If initialization fails, decrement the counter to keep it in sync | ||
async with self._usage_lock: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can directly use the new function _decrement_usage here. The same applies to cluster.py changes as well. |
||
self._usage_counter -= 1 | ||
raise | ||
|
||
async def _decrement_usage(self) -> int: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A helper method is required so we can use it in the |
||
""" | ||
Helper coroutine to decrement the usage counter while holding the lock. | ||
Returns the new value of the usage counter. | ||
""" | ||
async with self._usage_lock: | ||
self._usage_counter -= 1 | ||
return self._usage_counter | ||
|
||
async def __aexit__(self, exc_type, exc_value, traceback): | ||
await self.aclose() | ||
""" | ||
Async context manager exit. Decrements a usage counter. If this is the | ||
last exit (counter becomes zero), the client closes its connection pool. | ||
""" | ||
current_usage = await asyncio.shield(self._decrement_usage()) | ||
if current_usage == 0: | ||
# This was the last active context, so disconnect the pool. | ||
await asyncio.shield(self.aclose()) | ||
|
||
_DEL_MESSAGE = "Unclosed Redis client" | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import asyncio | ||
|
||
import pytest | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_usage_counter(r): | ||
async def dummy_task(): | ||
async with r: | ||
await asyncio.sleep(0.01) | ||
|
||
tasks = [dummy_task() for _ in range(20)] | ||
await asyncio.gather(*tasks) | ||
|
||
# After all tasks have completed, the usage counter should be back to zero. | ||
assert r._usage_counter == 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest adding another function _increment_usage for this operation. It will be easier to read and follow the code if here you call _increment_usage and below in the except clause you call _decrement_usage. The same applies to cluster.py changes as well.