2
2
import copy
3
3
import enum
4
4
import inspect
5
- import os
6
5
import socket
7
6
import ssl
8
7
import sys
9
- import threading
10
8
import weakref
11
9
from abc import abstractmethod
12
10
from itertools import chain
41
39
from redis .exceptions import (
42
40
AuthenticationError ,
43
41
AuthenticationWrongNumberOfArgsError ,
44
- ChildDeadlockedError ,
45
42
ConnectionError ,
46
43
DataError ,
47
44
RedisError ,
@@ -97,7 +94,6 @@ class AbstractConnection:
97
94
"""Manages communication to and from a Redis server"""
98
95
99
96
__slots__ = (
100
- "pid" ,
101
97
"db" ,
102
98
"username" ,
103
99
"client_name" ,
@@ -158,7 +154,6 @@ def __init__(
158
154
"1. 'password' and (optional) 'username'\n "
159
155
"2. 'credential_provider'"
160
156
)
161
- self .pid = os .getpid ()
162
157
self .db = db
163
158
self .client_name = client_name
164
159
self .lib_name = lib_name
@@ -381,12 +376,11 @@ async def disconnect(self, nowait: bool = False) -> None:
381
376
if not self .is_connected :
382
377
return
383
378
try :
384
- if os .getpid () == self .pid :
385
- self ._writer .close () # type: ignore[union-attr]
386
- # wait for close to finish, except when handling errors and
387
- # forcefully disconnecting.
388
- if not nowait :
389
- await self ._writer .wait_closed () # type: ignore[union-attr]
379
+ self ._writer .close () # type: ignore[union-attr]
380
+ # wait for close to finish, except when handling errors and
381
+ # forcefully disconnecting.
382
+ if not nowait :
383
+ await self ._writer .wait_closed () # type: ignore[union-attr]
390
384
except OSError :
391
385
pass
392
386
finally :
@@ -1004,15 +998,6 @@ def __init__(
1004
998
self .connection_kwargs = connection_kwargs
1005
999
self .max_connections = max_connections
1006
1000
1007
- # a lock to protect the critical section in _checkpid().
1008
- # this lock is acquired when the process id changes, such as
1009
- # after a fork. during this time, multiple threads in the child
1010
- # process could attempt to acquire this lock. the first thread
1011
- # to acquire the lock will reset the data structures and lock
1012
- # object of this pool. subsequent threads acquiring this lock
1013
- # will notice the first thread already did the work and simply
1014
- # release the lock.
1015
- self ._fork_lock = threading .Lock ()
1016
1001
self ._lock = asyncio .Lock ()
1017
1002
self ._created_connections : int
1018
1003
self ._available_connections : List [AbstractConnection ]
@@ -1032,67 +1017,8 @@ def reset(self):
1032
1017
self ._available_connections = []
1033
1018
self ._in_use_connections = set ()
1034
1019
1035
- # this must be the last operation in this method. while reset() is
1036
- # called when holding _fork_lock, other threads in this process
1037
- # can call _checkpid() which compares self.pid and os.getpid() without
1038
- # holding any lock (for performance reasons). keeping this assignment
1039
- # as the last operation ensures that those other threads will also
1040
- # notice a pid difference and block waiting for the first thread to
1041
- # release _fork_lock. when each of these threads eventually acquire
1042
- # _fork_lock, they will notice that another thread already called
1043
- # reset() and they will immediately release _fork_lock and continue on.
1044
- self .pid = os .getpid ()
1045
-
1046
- def _checkpid (self ):
1047
- # _checkpid() attempts to keep ConnectionPool fork-safe on modern
1048
- # systems. this is called by all ConnectionPool methods that
1049
- # manipulate the pool's state such as get_connection() and release().
1050
- #
1051
- # _checkpid() determines whether the process has forked by comparing
1052
- # the current process id to the process id saved on the ConnectionPool
1053
- # instance. if these values are the same, _checkpid() simply returns.
1054
- #
1055
- # when the process ids differ, _checkpid() assumes that the process
1056
- # has forked and that we're now running in the child process. the child
1057
- # process cannot use the parent's file descriptors (e.g., sockets).
1058
- # therefore, when _checkpid() sees the process id change, it calls
1059
- # reset() in order to reinitialize the child's ConnectionPool. this
1060
- # will cause the child to make all new connection objects.
1061
- #
1062
- # _checkpid() is protected by self._fork_lock to ensure that multiple
1063
- # threads in the child process do not call reset() multiple times.
1064
- #
1065
- # there is an extremely small chance this could fail in the following
1066
- # scenario:
1067
- # 1. process A calls _checkpid() for the first time and acquires
1068
- # self._fork_lock.
1069
- # 2. while holding self._fork_lock, process A forks (the fork()
1070
- # could happen in a different thread owned by process A)
1071
- # 3. process B (the forked child process) inherits the
1072
- # ConnectionPool's state from the parent. that state includes
1073
- # a locked _fork_lock. process B will not be notified when
1074
- # process A releases the _fork_lock and will thus never be
1075
- # able to acquire the _fork_lock.
1076
- #
1077
- # to mitigate this possible deadlock, _checkpid() will only wait 5
1078
- # seconds to acquire _fork_lock. if _fork_lock cannot be acquired in
1079
- # that time it is assumed that the child is deadlocked and a
1080
- # redis.ChildDeadlockedError error is raised.
1081
- if self .pid != os .getpid ():
1082
- acquired = self ._fork_lock .acquire (timeout = 5 )
1083
- if not acquired :
1084
- raise ChildDeadlockedError
1085
- # reset() the instance for the new process if another thread
1086
- # hasn't already done so
1087
- try :
1088
- if self .pid != os .getpid ():
1089
- self .reset ()
1090
- finally :
1091
- self ._fork_lock .release ()
1092
-
1093
1020
async def get_connection (self , command_name , * keys , ** options ):
1094
1021
"""Get a connection from the pool"""
1095
- self ._checkpid ()
1096
1022
async with self ._lock :
1097
1023
try :
1098
1024
connection = self ._available_connections .pop ()
@@ -1141,7 +1067,6 @@ def make_connection(self):
1141
1067
1142
1068
async def release (self , connection : AbstractConnection ):
1143
1069
"""Releases the connection back to the pool"""
1144
- self ._checkpid ()
1145
1070
async with self ._lock :
1146
1071
try :
1147
1072
self ._in_use_connections .remove (connection )
@@ -1150,18 +1075,7 @@ async def release(self, connection: AbstractConnection):
1150
1075
# that the pool doesn't actually own
1151
1076
pass
1152
1077
1153
- if self .owns_connection (connection ):
1154
- self ._available_connections .append (connection )
1155
- else :
1156
- # pool doesn't own this connection. do not add it back
1157
- # to the pool and decrement the count so that another
1158
- # connection can take its place if needed
1159
- self ._created_connections -= 1
1160
- await connection .disconnect ()
1161
- return
1162
-
1163
- def owns_connection (self , connection : AbstractConnection ):
1164
- return connection .pid == self .pid
1078
+ self ._available_connections .append (connection )
1165
1079
1166
1080
async def disconnect (self , inuse_connections : bool = True ):
1167
1081
"""
@@ -1171,7 +1085,6 @@ async def disconnect(self, inuse_connections: bool = True):
1171
1085
current in use, potentially by other tasks. Otherwise only disconnect
1172
1086
connections that are idle in the pool.
1173
1087
"""
1174
- self ._checkpid ()
1175
1088
async with self ._lock :
1176
1089
if inuse_connections :
1177
1090
connections : Iterable [AbstractConnection ] = chain (
@@ -1259,17 +1172,6 @@ def reset(self):
1259
1172
# disconnect them later.
1260
1173
self ._connections = []
1261
1174
1262
- # this must be the last operation in this method. while reset() is
1263
- # called when holding _fork_lock, other threads in this process
1264
- # can call _checkpid() which compares self.pid and os.getpid() without
1265
- # holding any lock (for performance reasons). keeping this assignment
1266
- # as the last operation ensures that those other threads will also
1267
- # notice a pid difference and block waiting for the first thread to
1268
- # release _fork_lock. when each of these threads eventually acquire
1269
- # _fork_lock, they will notice that another thread already called
1270
- # reset() and they will immediately release _fork_lock and continue on.
1271
- self .pid = os .getpid ()
1272
-
1273
1175
def make_connection (self ):
1274
1176
"""Make a fresh connection."""
1275
1177
connection = self .connection_class (** self .connection_kwargs )
@@ -1288,8 +1190,6 @@ async def get_connection(self, command_name, *keys, **options):
1288
1190
create new connections when we need to, i.e.: the actual number of
1289
1191
connections will only increase in response to demand.
1290
1192
"""
1291
- # Make sure we haven't changed process.
1292
- self ._checkpid ()
1293
1193
1294
1194
# Try and get a connection from the pool. If one isn't available within
1295
1195
# self.timeout then raise a ``ConnectionError``.
@@ -1331,17 +1231,6 @@ async def get_connection(self, command_name, *keys, **options):
1331
1231
1332
1232
async def release (self , connection : AbstractConnection ):
1333
1233
"""Releases the connection back to the pool."""
1334
- # Make sure we haven't changed process.
1335
- self ._checkpid ()
1336
- if not self .owns_connection (connection ):
1337
- # pool doesn't own this connection. do not add it back
1338
- # to the pool. instead add a None value which is a placeholder
1339
- # that will cause the pool to recreate the connection if
1340
- # its needed.
1341
- await connection .disconnect ()
1342
- self .pool .put_nowait (None )
1343
- return
1344
-
1345
1234
# Put the connection back into the pool.
1346
1235
try :
1347
1236
self .pool .put_nowait (connection )
@@ -1352,7 +1241,6 @@ async def release(self, connection: AbstractConnection):
1352
1241
1353
1242
async def disconnect (self , inuse_connections : bool = True ):
1354
1243
"""Disconnects all connections in the pool."""
1355
- self ._checkpid ()
1356
1244
async with self ._lock :
1357
1245
resp = await asyncio .gather (
1358
1246
* (connection .disconnect () for connection in self ._connections ),
0 commit comments