Skip to content

Commit ded9f7c

Browse files
async fixes, remove __del__ and other things (#2870)
* Use correct redis url if not default when creating Connection * Make resource-warning __del__ code safer during shutdown * Remove __del__ handler, fix pubsub weakref callback handling * Clarify comment, since there is no __del__ on asyncio.connection.ConnectionPool * Remove remaining __del__ from async parser. They are not needed. * make connect callback methods internal * similarly make non-async connect callbacks internal, use same system as for async. * Reformat __del__()
1 parent c46a28d commit ded9f7c

File tree

8 files changed

+62
-39
lines changed

8 files changed

+62
-39
lines changed

redis/_parsers/base.py

-6
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,6 @@ def __init__(self, socket_read_size: int):
138138
self._stream: Optional[StreamReader] = None
139139
self._read_size = socket_read_size
140140

141-
def __del__(self):
142-
try:
143-
self.on_disconnect()
144-
except Exception:
145-
pass
146-
147141
async def can_read_destructive(self) -> bool:
148142
raise NotImplementedError()
149143

redis/asyncio/client.py

+18-10
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ def __init__(
227227
lib_version: Optional[str] = get_lib_version(),
228228
username: Optional[str] = None,
229229
retry: Optional[Retry] = None,
230-
# deprecated. create a pool and use connection_pool instead
231230
auto_close_connection_pool: Optional[bool] = None,
232231
redis_connect_func=None,
233232
credential_provider: Optional[CredentialProvider] = None,
@@ -241,7 +240,9 @@ def __init__(
241240
To retry on TimeoutError, `retry_on_timeout` can also be set to `True`.
242241
"""
243242
kwargs: Dict[str, Any]
244-
243+
# auto_close_connection_pool only has an effect if connection_pool is
244+
# None. It is assumed that if connection_pool is not None, the user
245+
# wants to manage the connection pool themselves.
245246
if auto_close_connection_pool is not None:
246247
warnings.warn(
247248
DeprecationWarning(
@@ -531,13 +532,20 @@ async def __aexit__(self, exc_type, exc_value, traceback):
531532

532533
_DEL_MESSAGE = "Unclosed Redis client"
533534

534-
def __del__(self, _warnings: Any = warnings) -> None:
535+
# passing _warnings and _grl as argument default since they may be gone
536+
# by the time __del__ is called at shutdown
537+
def __del__(
538+
self,
539+
_warn: Any = warnings.warn,
540+
_grl: Any = asyncio.get_running_loop,
541+
) -> None:
535542
if hasattr(self, "connection") and (self.connection is not None):
536-
_warnings.warn(
537-
f"Unclosed client session {self!r}", ResourceWarning, source=self
538-
)
539-
context = {"client": self, "message": self._DEL_MESSAGE}
540-
asyncio.get_running_loop().call_exception_handler(context)
543+
_warn(f"Unclosed client session {self!r}", ResourceWarning, source=self)
544+
try:
545+
context = {"client": self, "message": self._DEL_MESSAGE}
546+
_grl().call_exception_handler(context)
547+
except RuntimeError:
548+
pass
541549

542550
async def aclose(self, close_connection_pool: Optional[bool] = None) -> None:
543551
"""
@@ -786,7 +794,7 @@ async def aclose(self):
786794
async with self._lock:
787795
if self.connection:
788796
await self.connection.disconnect()
789-
self.connection.clear_connect_callbacks()
797+
self.connection._deregister_connect_callback(self.on_connect)
790798
await self.connection_pool.release(self.connection)
791799
self.connection = None
792800
self.channels = {}
@@ -849,7 +857,7 @@ async def connect(self):
849857
)
850858
# register a callback that re-subscribes to any channels we
851859
# were listening to when we were disconnected
852-
self.connection.register_connect_callback(self.on_connect)
860+
self.connection._register_connect_callback(self.on_connect)
853861
else:
854862
await self.connection.connect()
855863
if self.push_handler_func is not None and not HIREDIS_AVAILABLE:

redis/asyncio/cluster.py

+17-10
Original file line numberDiff line numberDiff line change
@@ -433,14 +433,18 @@ def __await__(self) -> Generator[Any, None, "RedisCluster"]:
433433

434434
_DEL_MESSAGE = "Unclosed RedisCluster client"
435435

436-
def __del__(self) -> None:
436+
def __del__(
437+
self,
438+
_warn: Any = warnings.warn,
439+
_grl: Any = asyncio.get_running_loop,
440+
) -> None:
437441
if hasattr(self, "_initialize") and not self._initialize:
438-
warnings.warn(f"{self._DEL_MESSAGE} {self!r}", ResourceWarning, source=self)
442+
_warn(f"{self._DEL_MESSAGE} {self!r}", ResourceWarning, source=self)
439443
try:
440444
context = {"client": self, "message": self._DEL_MESSAGE}
441-
asyncio.get_running_loop().call_exception_handler(context)
445+
_grl().call_exception_handler(context)
442446
except RuntimeError:
443-
...
447+
pass
444448

445449
async def on_connect(self, connection: Connection) -> None:
446450
await connection.on_connect()
@@ -969,17 +973,20 @@ def __eq__(self, obj: Any) -> bool:
969973

970974
_DEL_MESSAGE = "Unclosed ClusterNode object"
971975

972-
def __del__(self) -> None:
976+
def __del__(
977+
self,
978+
_warn: Any = warnings.warn,
979+
_grl: Any = asyncio.get_running_loop,
980+
) -> None:
973981
for connection in self._connections:
974982
if connection.is_connected:
975-
warnings.warn(
976-
f"{self._DEL_MESSAGE} {self!r}", ResourceWarning, source=self
977-
)
983+
_warn(f"{self._DEL_MESSAGE} {self!r}", ResourceWarning, source=self)
984+
978985
try:
979986
context = {"client": self, "message": self._DEL_MESSAGE}
980-
asyncio.get_running_loop().call_exception_handler(context)
987+
_grl().call_exception_handler(context)
981988
except RuntimeError:
982-
...
989+
pass
983990
break
984991

985992
async def disconnect(self) -> None:

redis/asyncio/connection.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,16 @@ def repr_pieces(self):
216216
def is_connected(self):
217217
return self._reader is not None and self._writer is not None
218218

219-
def register_connect_callback(self, callback):
220-
self._connect_callbacks.append(weakref.WeakMethod(callback))
219+
def _register_connect_callback(self, callback):
220+
wm = weakref.WeakMethod(callback)
221+
if wm not in self._connect_callbacks:
222+
self._connect_callbacks.append(wm)
221223

222-
def clear_connect_callbacks(self):
223-
self._connect_callbacks = []
224+
def _deregister_connect_callback(self, callback):
225+
try:
226+
self._connect_callbacks.remove(weakref.WeakMethod(callback))
227+
except ValueError:
228+
pass
224229

225230
def set_parser(self, parser_class: Type[BaseParser]) -> None:
226231
"""
@@ -263,6 +268,8 @@ async def connect(self):
263268

264269
# run any user callbacks. right now the only internal callback
265270
# is for pubsub channel/pattern resubscription
271+
# first, remove any dead weakrefs
272+
self._connect_callbacks = [ref for ref in self._connect_callbacks if ref()]
266273
for ref in self._connect_callbacks:
267274
callback = ref()
268275
task = callback(self)

redis/client.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ def __del__(self):
690690
def reset(self):
691691
if self.connection:
692692
self.connection.disconnect()
693-
self.connection.clear_connect_callbacks()
693+
self.connection._deregister_connect_callback(self.on_connect)
694694
self.connection_pool.release(self.connection)
695695
self.connection = None
696696
self.health_check_response_counter = 0
@@ -748,7 +748,7 @@ def execute_command(self, *args):
748748
)
749749
# register a callback that re-subscribes to any channels we
750750
# were listening to when we were disconnected
751-
self.connection.register_connect_callback(self.on_connect)
751+
self.connection._register_connect_callback(self.on_connect)
752752
if self.push_handler_func is not None and not HIREDIS_AVAILABLE:
753753
self.connection._parser.set_push_handler(self.push_handler_func)
754754
connection = self.connection

redis/cluster.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1775,7 +1775,7 @@ def execute_command(self, *args):
17751775
)
17761776
# register a callback that re-subscribes to any channels we
17771777
# were listening to when we were disconnected
1778-
self.connection.register_connect_callback(self.on_connect)
1778+
self.connection._register_connect_callback(self.on_connect)
17791779
if self.push_handler_func is not None and not HIREDIS_AVAILABLE:
17801780
self.connection._parser.set_push_handler(self.push_handler_func)
17811781
connection = self.connection

redis/connection.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,16 @@ def _construct_command_packer(self, packer):
237237
else:
238238
return PythonRespSerializer(self._buffer_cutoff, self.encoder.encode)
239239

240-
def register_connect_callback(self, callback):
241-
self._connect_callbacks.append(weakref.WeakMethod(callback))
240+
def _register_connect_callback(self, callback):
241+
wm = weakref.WeakMethod(callback)
242+
if wm not in self._connect_callbacks:
243+
self._connect_callbacks.append(wm)
242244

243-
def clear_connect_callbacks(self):
244-
self._connect_callbacks = []
245+
def _deregister_connect_callback(self, callback):
246+
try:
247+
self._connect_callbacks.remove(weakref.WeakMethod(callback))
248+
except ValueError:
249+
pass
245250

246251
def set_parser(self, parser_class):
247252
"""
@@ -279,6 +284,8 @@ def connect(self):
279284

280285
# run any user callbacks. right now the only internal callback
281286
# is for pubsub channel/pattern resubscription
287+
# first, remove any dead weakrefs
288+
self._connect_callbacks = [ref for ref in self._connect_callbacks if ref()]
282289
for ref in self._connect_callbacks:
283290
callback = ref()
284291
if callback:

tests/test_asyncio/test_sentinel_managed_connection.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
pytestmark = pytest.mark.asyncio
1111

1212

13-
async def test_connect_retry_on_timeout_error():
13+
async def test_connect_retry_on_timeout_error(connect_args):
1414
"""Test that the _connect function is retried in case of a timeout"""
1515
connection_pool = mock.AsyncMock()
1616
connection_pool.get_master_address = mock.AsyncMock(
17-
return_value=("localhost", 6379)
17+
return_value=(connect_args["host"], connect_args["port"])
1818
)
1919
conn = SentinelManagedConnection(
2020
retry_on_timeout=True,

0 commit comments

Comments
 (0)