Skip to content

Commit c46a28d

Browse files
Provide aclose() / close() for classes requiring lifetime management (#2898)
* Define `aclose()` methods instead of `close()` for async Redis() * update examples to use `aclose()` * Update tests to use Redis.aclose() * Add aclose() to asyncio.client.PubSub close() and reset() retained as aliases * Add aclose method to asyncio.RedisCluster * Add aclose() to asyncio.client.Pipeline * add `close()` method to sync Pipeline * add `aclose()` to asyncio.connection.ConnectionPool * Add `close()` method to redis.ConnectionPool * Deprecate older functions. * changes.txt * fix unittest * fix typo * Update docs
1 parent 6207641 commit c46a28d

16 files changed

+294
-91
lines changed

CHANGES

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
* Add 'aclose()' methods to async classes, deprecate async close().
12
* Fix #2831, add auto_close_connection_pool=True arg to asyncio.Redis.from_url()
23
* Fix incorrect redis.asyncio.Cluster type hint for `retry_on_error`
34
* Fix dead weakref in sentinel connection causing ReferenceError (#2767)

docs/examples/asyncio_examples.ipynb

+14-14
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"\n",
1616
"## Connecting and Disconnecting\n",
1717
"\n",
18-
"Utilizing asyncio Redis requires an explicit disconnect of the connection since there is no asyncio deconstructor magic method. By default, a connection pool is created on `redis.Redis()` and attached to this `Redis` instance. The connection pool closes automatically on the call to `Redis.close` which disconnects all connections."
18+
"Utilizing asyncio Redis requires an explicit disconnect of the connection since there is no asyncio deconstructor magic method. By default, a connection pool is created on `redis.Redis()` and attached to this `Redis` instance. The connection pool closes automatically on the call to `Redis.aclose` which disconnects all connections."
1919
]
2020
},
2121
{
@@ -39,9 +39,9 @@
3939
"source": [
4040
"import redis.asyncio as redis\n",
4141
"\n",
42-
"connection = redis.Redis()\n",
43-
"print(f\"Ping successful: {await connection.ping()}\")\n",
44-
"await connection.close()"
42+
"client = redis.Redis()\n",
43+
"print(f\"Ping successful: {await client.ping()}\")\n",
44+
"await client.aclose()"
4545
]
4646
},
4747
{
@@ -60,8 +60,8 @@
6060
"import redis.asyncio as redis\n",
6161
"\n",
6262
"pool = redis.ConnectionPool.from_url(\"redis://localhost\")\n",
63-
"connection = redis.Redis.from_pool(pool)\n",
64-
"await connection.close()"
63+
"client = redis.Redis.from_pool(pool)\n",
64+
"await client.close()"
6565
]
6666
},
6767
{
@@ -91,11 +91,11 @@
9191
"import redis.asyncio as redis\n",
9292
"\n",
9393
"pool = redis.ConnectionPool.from_url(\"redis://localhost\")\n",
94-
"connection1 = redis.Redis(connection_pool=pool)\n",
95-
"connection2 = redis.Redis(connection_pool=pool)\n",
96-
"await connection1.close()\n",
97-
"await connection2.close()\n",
98-
"await pool.disconnect()"
94+
"client1 = redis.Redis(connection_pool=pool)\n",
95+
"client2 = redis.Redis(connection_pool=pool)\n",
96+
"await client1.aclose()\n",
97+
"await client2.aclose()\n",
98+
"await pool.aclose()"
9999
]
100100
},
101101
{
@@ -113,9 +113,9 @@
113113
"source": [
114114
"import redis.asyncio as redis\n",
115115
"\n",
116-
"connection = redis.Redis(protocol=3)\n",
117-
"await connection.close()\n",
118-
"await connection.ping()"
116+
"client = redis.Redis(protocol=3)\n",
117+
"await client.aclose()\n",
118+
"await client.ping()"
119119
]
120120
},
121121
{

redis/asyncio/client.py

+32-14
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
List,
1515
Mapping,
1616
MutableMapping,
17-
NoReturn,
1817
Optional,
1918
Set,
2019
Tuple,
@@ -65,6 +64,7 @@
6564
from redis.utils import (
6665
HIREDIS_AVAILABLE,
6766
_set_info_logger,
67+
deprecated_function,
6868
get_lib_version,
6969
safe_str,
7070
str_if_bytes,
@@ -527,7 +527,7 @@ async def __aenter__(self: _RedisT) -> _RedisT:
527527
return await self.initialize()
528528

529529
async def __aexit__(self, exc_type, exc_value, traceback):
530-
await self.close()
530+
await self.aclose()
531531

532532
_DEL_MESSAGE = "Unclosed Redis client"
533533

@@ -539,7 +539,7 @@ def __del__(self, _warnings: Any = warnings) -> None:
539539
context = {"client": self, "message": self._DEL_MESSAGE}
540540
asyncio.get_running_loop().call_exception_handler(context)
541541

542-
async def close(self, close_connection_pool: Optional[bool] = None) -> None:
542+
async def aclose(self, close_connection_pool: Optional[bool] = None) -> None:
543543
"""
544544
Closes Redis client connection
545545
@@ -557,6 +557,13 @@ async def close(self, close_connection_pool: Optional[bool] = None) -> None:
557557
):
558558
await self.connection_pool.disconnect()
559559

560+
@deprecated_function(version="5.0.0", reason="Use aclose() instead", name="close")
561+
async def close(self, close_connection_pool: Optional[bool] = None) -> None:
562+
"""
563+
Alias for aclose(), for backwards compatibility
564+
"""
565+
await self.aclose(close_connection_pool)
566+
560567
async def _send_command_parse_response(self, conn, command_name, *args, **options):
561568
"""
562569
Send a command and parse the response
@@ -764,13 +771,18 @@ async def __aenter__(self):
764771
return self
765772

766773
async def __aexit__(self, exc_type, exc_value, traceback):
767-
await self.reset()
774+
await self.aclose()
768775

769776
def __del__(self):
770777
if self.connection:
771778
self.connection.clear_connect_callbacks()
772779

773-
async def reset(self):
780+
async def aclose(self):
781+
# In case a connection property does not yet exist
782+
# (due to a crash earlier in the Redis() constructor), return
783+
# immediately as there is nothing to clean-up.
784+
if not hasattr(self, "connection"):
785+
return
774786
async with self._lock:
775787
if self.connection:
776788
await self.connection.disconnect()
@@ -782,13 +794,15 @@ async def reset(self):
782794
self.patterns = {}
783795
self.pending_unsubscribe_patterns = set()
784796

785-
def close(self) -> Awaitable[NoReturn]:
786-
# In case a connection property does not yet exist
787-
# (due to a crash earlier in the Redis() constructor), return
788-
# immediately as there is nothing to clean-up.
789-
if not hasattr(self, "connection"):
790-
return
791-
return self.reset()
797+
@deprecated_function(version="5.0.0", reason="Use aclose() instead", name="close")
798+
async def close(self) -> None:
799+
"""Alias for aclose(), for backwards compatibility"""
800+
await self.aclose()
801+
802+
@deprecated_function(version="5.0.0", reason="Use aclose() instead", name="reset")
803+
async def reset(self) -> None:
804+
"""Alias for aclose(), for backwards compatibility"""
805+
await self.aclose()
792806

793807
async def on_connect(self, connection: Connection):
794808
"""Re-subscribe to any channels and patterns previously subscribed to"""
@@ -1232,6 +1246,10 @@ async def reset(self):
12321246
await self.connection_pool.release(self.connection)
12331247
self.connection = None
12341248

1249+
async def aclose(self) -> None:
1250+
"""Alias for reset(), a standard method name for cleanup"""
1251+
await self.reset()
1252+
12351253
def multi(self):
12361254
"""
12371255
Start a transactional block of the pipeline after WATCH commands
@@ -1264,14 +1282,14 @@ async def _disconnect_reset_raise(self, conn, error):
12641282
# valid since this connection has died. raise a WatchError, which
12651283
# indicates the user should retry this transaction.
12661284
if self.watching:
1267-
await self.reset()
1285+
await self.aclose()
12681286
raise WatchError(
12691287
"A ConnectionError occurred on while watching one or more keys"
12701288
)
12711289
# if retry_on_timeout is not set, or the error is not
12721290
# a TimeoutError, raise it
12731291
if not (conn.retry_on_timeout and isinstance(error, TimeoutError)):
1274-
await self.reset()
1292+
await self.aclose()
12751293
raise
12761294

12771295
async def immediate_execute_command(self, *args, **options):

redis/asyncio/cluster.py

+23-12
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@
6262
TryAgainError,
6363
)
6464
from redis.typing import AnyKeyT, EncodableT, KeyT
65-
from redis.utils import dict_merge, get_lib_version, safe_str, str_if_bytes
65+
from redis.utils import (
66+
deprecated_function,
67+
dict_merge,
68+
get_lib_version,
69+
safe_str,
70+
str_if_bytes,
71+
)
6672

6773
TargetNodesT = TypeVar(
6874
"TargetNodesT", str, "ClusterNode", List["ClusterNode"], Dict[Any, "ClusterNode"]
@@ -395,27 +401,32 @@ async def initialize(self) -> "RedisCluster":
395401
)
396402
self._initialize = False
397403
except BaseException:
398-
await self.nodes_manager.close()
399-
await self.nodes_manager.close("startup_nodes")
404+
await self.nodes_manager.aclose()
405+
await self.nodes_manager.aclose("startup_nodes")
400406
raise
401407
return self
402408

403-
async def close(self) -> None:
409+
async def aclose(self) -> None:
404410
"""Close all connections & client if initialized."""
405411
if not self._initialize:
406412
if not self._lock:
407413
self._lock = asyncio.Lock()
408414
async with self._lock:
409415
if not self._initialize:
410416
self._initialize = True
411-
await self.nodes_manager.close()
412-
await self.nodes_manager.close("startup_nodes")
417+
await self.nodes_manager.aclose()
418+
await self.nodes_manager.aclose("startup_nodes")
419+
420+
@deprecated_function(version="5.0.0", reason="Use aclose() instead", name="close")
421+
async def close(self) -> None:
422+
"""alias for aclose() for backwards compatibility"""
423+
await self.aclose()
413424

414425
async def __aenter__(self) -> "RedisCluster":
415426
return await self.initialize()
416427

417428
async def __aexit__(self, exc_type: None, exc_value: None, traceback: None) -> None:
418-
await self.close()
429+
await self.aclose()
419430

420431
def __await__(self) -> Generator[Any, None, "RedisCluster"]:
421432
return self.initialize().__await__()
@@ -767,13 +778,13 @@ async def _execute_command(
767778
self.nodes_manager.startup_nodes.pop(target_node.name, None)
768779
# Hard force of reinitialize of the node/slots setup
769780
# and try again with the new setup
770-
await self.close()
781+
await self.aclose()
771782
raise
772783
except ClusterDownError:
773784
# ClusterDownError can occur during a failover and to get
774785
# self-healed, we will try to reinitialize the cluster layout
775786
# and retry executing the command
776-
await self.close()
787+
await self.aclose()
777788
await asyncio.sleep(0.25)
778789
raise
779790
except MovedError as e:
@@ -790,7 +801,7 @@ async def _execute_command(
790801
self.reinitialize_steps
791802
and self.reinitialize_counter % self.reinitialize_steps == 0
792803
):
793-
await self.close()
804+
await self.aclose()
794805
# Reset the counter
795806
self.reinitialize_counter = 0
796807
else:
@@ -1323,7 +1334,7 @@ async def initialize(self) -> None:
13231334
# If initialize was called after a MovedError, clear it
13241335
self._moved_exception = None
13251336

1326-
async def close(self, attr: str = "nodes_cache") -> None:
1337+
async def aclose(self, attr: str = "nodes_cache") -> None:
13271338
self.default_node = None
13281339
await asyncio.gather(
13291340
*(
@@ -1471,7 +1482,7 @@ async def execute(
14711482
if type(e) in self.__class__.ERRORS_ALLOW_RETRY:
14721483
# Try again with the new cluster setup.
14731484
exception = e
1474-
await self._client.close()
1485+
await self._client.aclose()
14751486
await asyncio.sleep(0.25)
14761487
else:
14771488
# All other errors should be raised.

redis/asyncio/connection.py

+4
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,10 @@ async def disconnect(self, inuse_connections: bool = True):
10951095
if exc:
10961096
raise exc
10971097

1098+
async def aclose(self) -> None:
1099+
"""Close the pool, disconnecting all connections"""
1100+
await self.disconnect()
1101+
10981102
def set_retry(self, retry: "Retry") -> None:
10991103
for conn in self._available_connections:
11001104
conn.retry = retry

redis/client.py

+4
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,10 @@ def reset(self):
12171217
self.connection_pool.release(self.connection)
12181218
self.connection = None
12191219

1220+
def close(self):
1221+
"""Close the pipeline"""
1222+
self.reset()
1223+
12201224
def multi(self):
12211225
"""
12221226
Start a transactional block of the pipeline after WATCH commands

redis/connection.py

+4
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,10 @@ def disconnect(self, inuse_connections=True):
11541154
for connection in connections:
11551155
connection.disconnect()
11561156

1157+
def close(self) -> None:
1158+
"""Close the pool, disconnecting all connections"""
1159+
self.disconnect()
1160+
11571161
def set_retry(self, retry: "Retry") -> None:
11581162
self.connection_kwargs.update({"retry": retry})
11591163
for conn in self._available_connections:

tests/test_asyncio/compat.py

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66
except AttributeError:
77
import mock
88

9+
try:
10+
from contextlib import aclosing
11+
except ImportError:
12+
import contextlib
13+
14+
@contextlib.asynccontextmanager
15+
async def aclosing(thing):
16+
try:
17+
yield thing
18+
finally:
19+
await thing.aclose()
20+
921

1022
def create_task(coroutine):
1123
return asyncio.create_task(coroutine)

tests/test_asyncio/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ async def teardown():
100100
# handle cases where a test disconnected a client
101101
# just manually retry the flushdb
102102
await client.flushdb()
103-
await client.close()
103+
await client.aclose()
104104
await client.connection_pool.disconnect()
105105
else:
106106
if flushdb:
@@ -110,7 +110,7 @@ async def teardown():
110110
# handle cases where a test disconnected a client
111111
# just manually retry the flushdb
112112
await client.flushdb(target_nodes="primaries")
113-
await client.close()
113+
await client.aclose()
114114

115115
teardown_clients.append(teardown)
116116
return client

0 commit comments

Comments
 (0)