Skip to content

Commit 7d7bd59

Browse files
committed
Support ping() method for pooled and persistent connections.
1 parent 2f9d655 commit 7d7bd59

6 files changed

Lines changed: 252 additions & 71 deletions

File tree

DBUtils/PersistentDB.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
failures: an optional exception class or a tuple of exception classes
4040
for which the connection failover mechanism shall be applied,
4141
if the default (OperationalError, InternalError) is not adequate
42+
ping: an optional flag controlling when connections are checked
43+
with the ping() method if such a method is available
44+
(0 = None = never, 1 = default = whenever it is requested,
45+
2 = when a cursor is created, 4 = when a query is executed,
46+
7 = always, and all other bit combinations of these values)
4247
closeable: if this is set to true, then closing connections will
4348
be allowed, but by default this will be silently ignored
4449
threadlocal: an optional class for representing thread-local data
@@ -129,7 +134,7 @@ class PersistentDB:
129134
version = __version__
130135

131136
def __init__(self, creator,
132-
maxusage=None, setsession=None, failures=None,
137+
maxusage=None, setsession=None, failures=None, ping=1,
133138
closeable=False, threadlocal=None, *args, **kwargs):
134139
"""Set up the persistent DB-API 2 connection generator.
135140
@@ -143,6 +148,10 @@ def __init__(self, creator,
143148
failures: an optional exception class or a tuple of exception classes
144149
for which the connection failover mechanism shall be applied,
145150
if the default (OperationalError, InternalError) is not adequate
151+
ping: determines when the connection should be checked with ping()
152+
(0 = None = never, 1 = default = whenever it is requested,
153+
2 = when a cursor is created, 4 = when a query is executed,
154+
7 = always, and all other bit combinations of these values)
146155
closeable: if this is set to true, then closing connections will
147156
be allowed, but by default this will be silently ignored
148157
threadlocal: an optional class for representing thread-local data
@@ -168,6 +177,7 @@ def __init__(self, creator,
168177
self._maxusage = maxusage
169178
self._setsession = setsession
170179
self._failures = failures
180+
self._ping = ping
171181
self._closeable = closeable
172182
self._args, self._kwargs = args, kwargs
173183
self.thread = (threadlocal or ThreadingLocal.local)()
@@ -176,7 +186,7 @@ def steady_connection(self):
176186
"""Get a steady, non-persistent DB-API 2 connection."""
177187
return connect(self._creator,
178188
self._maxusage, self._setsession,
179-
self._failures, self._closeable, None,
189+
self._failures, self._ping, self._closeable,
180190
*self._args, **self._kwargs)
181191

182192
def connection(self, shareable=False):
@@ -194,6 +204,7 @@ def connection(self, shareable=False):
194204
if not con.threadsafety():
195205
raise NotSupportedError("Database module is not thread-safe.")
196206
self.thread.connection = con
207+
con._ping_check()
197208
return con
198209

199210
def dedicated_connection(self):

DBUtils/PooledDB.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
failures: an optional exception class or a tuple of exception classes
5252
for which the connection failover mechanism shall be applied,
5353
if the default (OperationalError, InternalError) is not adequate
54+
ping: an optional flag controlling when connections are checked
55+
with the ping() method if such a method is available
56+
(0 = None = never, 1 = default = whenever fetched from the pool,
57+
2 = when a cursor is created, 4 = when a query is executed,
58+
7 = always, and all other bit combinations of these values)
5459
5560
The creator function or the connect function of the DB-API 2 compliant
5661
database module specified as the creator will receive any additional
@@ -157,7 +162,8 @@ class PooledDB:
157162
def __init__(self, creator,
158163
mincached=0, maxcached=0,
159164
maxshared=0, maxconnections=0, blocking=False,
160-
maxusage=None, setsession=None, failures=None,
165+
maxusage=None, setsession=None,
166+
failures=None, ping=1,
161167
*args, **kwargs):
162168
"""Set up the DB-API 2 connection pool.
163169
@@ -185,6 +191,10 @@ def __init__(self, creator,
185191
failures: an optional exception class or a tuple of exception classes
186192
for which the connection failover mechanism shall be applied,
187193
if the default (OperationalError, InternalError) is not adequate
194+
ping: determines when the connection should be checked with ping()
195+
(0 = None = never, 1 = default = whenever fetched from the pool,
196+
2 = when a cursor is created, 4 = when a query is executed,
197+
7 = always, and all other bit combinations of these values)
188198
args, kwargs: the parameters that shall be passed to the creator
189199
function or the connection constructor of the DB-API 2 module
190200
@@ -206,6 +216,7 @@ def __init__(self, creator,
206216
self._maxusage = maxusage
207217
self._setsession = setsession
208218
self._failures = failures
219+
self._ping = ping
209220
if mincached is None:
210221
mincached = 0
211222
if maxcached is None:
@@ -247,7 +258,7 @@ def steady_connection(self):
247258
"""Get a steady, unpooled DB-API 2 connection."""
248259
return connect(self._creator,
249260
self._maxusage, self._setsession,
250-
self._failures, True, None,
261+
self._failures, self._ping, True,
251262
*self._args, **self._kwargs)
252263

253264
def connection(self, shareable=True):
@@ -269,11 +280,14 @@ def connection(self, shareable=True):
269280
con = self._idle_cache.pop(0)
270281
except IndexError: # else get a fresh connection
271282
con = self.steady_connection()
283+
else:
284+
con._ping_check() # check this connection
272285
con = SharedDBConnection(con)
273286
self._connections += 1
274287
else: # shared cache full or no more connections allowed
275288
self._shared_cache.sort() # least shared connection first
276289
con = self._shared_cache.pop(0) # get it
290+
con.con._ping_check() # check the underlying connection
277291
con.share() # increase share of this connection
278292
# put the connection (back) into the shared cache
279293
self._shared_cache.append(con)
@@ -292,6 +306,8 @@ def connection(self, shareable=True):
292306
con = self._idle_cache.pop(0)
293307
except IndexError: # else get a fresh connection
294308
con = self.steady_connection()
309+
else:
310+
con._ping_check() # check connection
295311
con = PooledDedicatedDBConnection(self, con)
296312
self._connections += 1
297313
finally:

DBUtils/SteadyDB.py

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ class InvalidCursor(SteadyDBError):
103103
"""Database cursor is invalid."""
104104

105105

106-
def connect(creator, maxusage=None, setsession=None, failures=None,
107-
closeable=True, ping=None, *args, **kwargs):
106+
def connect(creator, maxusage=None, setsession=None,
107+
failures=None, ping=1, closeable=True, *args, **kwargs):
108108
"""A tough version of the connection constructor of a DB-API 2 module.
109109
110110
creator: either an arbitrary function returning new DB-API 2 compliant
@@ -118,25 +118,27 @@ def connect(creator, maxusage=None, setsession=None, failures=None,
118118
failures: an optional exception class or a tuple of exception classes
119119
for which the failover mechanism shall be applied, if the default
120120
(OperationalError, InternalError) is not adequate
121+
ping: determines when the connection should be checked with ping()
122+
(0 = None = never, 1 = default = when _ping_check() is called,
123+
2 = whenever a cursor is created, 4 = when a query is executed,
124+
7 = always, and all other bit combinations of these values)
121125
closeable: if this is set to false, then closing the connection will
122126
be silently ignored, but by default the connection can be closed
123-
ping: determines when the connection should be checked with ping()
124-
(0 = default = never, 1 = on cursor(), 2 = on execute(), 3 = both)
125127
args, kwargs: the parameters that shall be passed to the creator
126128
function or the connection constructor of the DB-API 2 module
127129
128130
"""
129-
return SteadyDBConnection(creator, maxusage, setsession, failures,
130-
closeable, ping, *args, **kwargs)
131+
return SteadyDBConnection(creator, maxusage, setsession,
132+
failures, ping, closeable, *args, **kwargs)
131133

132134

133135
class SteadyDBConnection:
134136
"""A "tough" version of DB-API 2 connections."""
135137

136138
version = __version__
137139

138-
def __init__(self, creator, maxusage=None, setsession=None, failures=None,
139-
closeable=True, ping=None, *args, **kwargs):
140+
def __init__(self, creator, maxusage=None, setsession=None,
141+
failures=None, ping=1, closeable=True, *args, **kwargs):
140142
"""Create a "tough" DB-API 2 connection."""
141143
# basic initialization to make finalizer work
142144
self._con = None
@@ -176,8 +178,8 @@ def __init__(self, creator, maxusage=None, setsession=None, failures=None,
176178
failures, tuple) and not issubclass(failures, Exception):
177179
raise TypeError("'failures' must be a tuple of exceptions.")
178180
self._failures = failures
181+
self._ping = isinstance(ping, int) and ping or 0
179182
self._closeable = closeable
180-
self._ping = ping or 0
181183
self._args, self._kwargs = args, kwargs
182184
self._store(self._create())
183185

@@ -298,6 +300,38 @@ def _close(self):
298300
pass
299301
self._closed = True
300302

303+
def _ping_check(self, ping=1, reconnect=True):
304+
"""Check whether the connection is still alive using ping().
305+
306+
If the the underlying connection is not active and the ping
307+
parameter is set accordingly, the connection will be recreated.
308+
309+
"""
310+
if ping & self._ping:
311+
try: # if possible, ping the connection
312+
alive = self._con.ping()
313+
except (AttributeError, IndexError, TypeError, ValueError):
314+
self._ping = 0 # ping() is not available
315+
alive = None
316+
reconnect = False
317+
except Exception:
318+
alive = False
319+
else:
320+
if alive is None:
321+
alive = True
322+
if alive:
323+
reconnect = False
324+
if reconnect:
325+
try: # try to reopen the connection
326+
con = self._create()
327+
except Exception:
328+
pass
329+
else:
330+
self._close()
331+
self._store(con)
332+
alive = True
333+
return alive
334+
301335
def dbapi(self):
302336
"""Return the underlying DB-API 2 module of the connection."""
303337
if self._dbapi is None:
@@ -344,39 +378,29 @@ def _cursor(self, *args, **kwargs):
344378
"""A "tough" version of the method cursor()."""
345379
# The args and kwargs are not part of the standard,
346380
# but some database modules seem to use these.
381+
self._ping_check(2)
347382
try:
348383
if self._maxusage:
349384
if self._usage >= self._maxusage:
350385
# the connection was used too often
351386
raise self._failure
352-
con = self._con
353-
if self._ping & 1:
354-
try: # if possible, ping the connection
355-
ping = con.ping()
356-
except (AttributeError, TypeError, ValueError):
357-
self._ping = 0 # ping() is not available
358-
ping = None
359-
except Exception:
360-
ping = False
361-
if ping is not None and not ping:
362-
raise self._failure # connection is dead
363-
cursor = con.cursor(*args, **kwargs) # try to get a cursor
387+
cursor = self._con.cursor(*args, **kwargs) # try to get a cursor
364388
except self._failures, error: # error in getting cursor
365389
try: # try to reopen the connection
366-
con2 = self._create()
390+
con = self._create()
367391
except Exception:
368392
pass
369393
else:
370394
try: # and try one more time to get a cursor
371-
cursor = con2.cursor(*args, **kwargs)
395+
cursor = con.cursor(*args, **kwargs)
372396
except Exception:
373397
pass
374398
else:
375399
self._close()
376-
self._store(con2)
400+
self._store(con)
377401
return cursor
378402
try:
379-
con2.close()
403+
con.close()
380404
except Exception:
381405
pass
382406
raise error # reraise the original error again
@@ -455,21 +479,12 @@ def _get_tough_method(self, name):
455479
def tough_method(*args, **kwargs):
456480
execute = name.startswith('execute')
457481
con = self._con
482+
con._ping_check(4)
458483
try:
459484
if con._maxusage:
460485
if con._usage >= con._maxusage:
461486
# the connection was used too often
462487
raise con._failure
463-
if con._ping & 2:
464-
try: # if possible, ping the connection
465-
ping = con._con.ping()
466-
except (AttributeError, TypeError, ValueError):
467-
self._ping = 0 # ping() is not available
468-
ping = None
469-
except Exception:
470-
ping = False
471-
if ping is not None and not ping:
472-
raise con._failure # connection is dead
473488
if execute:
474489
self._setsizes()
475490
method = getattr(self._cursor, name)

DBUtils/Tests/TestPersistentDB.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,58 @@ class threadlocal:
223223
persist = PersistentDB(dbapi, threadlocal=threadlocal)
224224
self.assert_(isinstance(persist.thread, threadlocal))
225225

226+
def test8_PingCheck(self):
227+
Connection = dbapi.Connection
228+
Connection.has_ping = True
229+
Connection.num_pings = 0
230+
persist = PersistentDB(dbapi, 0, None, None, 0, True)
231+
db = persist.connection()
232+
self.assert_(db._con.valid)
233+
self.assertEqual(Connection.num_pings, 0)
234+
db.close()
235+
db = persist.connection()
236+
self.assert_(not db._con.valid)
237+
self.assertEqual(Connection.num_pings, 0)
238+
persist = PersistentDB(dbapi, 0, None, None, 1, True)
239+
db = persist.connection()
240+
self.assert_(db._con.valid)
241+
self.assertEqual(Connection.num_pings, 1)
242+
db.close()
243+
db = persist.connection()
244+
self.assert_(db._con.valid)
245+
self.assertEqual(Connection.num_pings, 2)
246+
persist = PersistentDB(dbapi, 0, None, None, 2, True)
247+
db = persist.connection()
248+
self.assert_(db._con.valid)
249+
self.assertEqual(Connection.num_pings, 2)
250+
db.close()
251+
db = persist.connection()
252+
self.assert_(not db._con.valid)
253+
self.assertEqual(Connection.num_pings, 2)
254+
cursor = db.cursor()
255+
self.assert_(db._con.valid)
256+
self.assertEqual(Connection.num_pings, 3)
257+
cursor.execute('select test')
258+
self.assert_(db._con.valid)
259+
self.assertEqual(Connection.num_pings, 3)
260+
persist = PersistentDB(dbapi, 0, None, None, 4, True)
261+
db = persist.connection()
262+
self.assert_(db._con.valid)
263+
self.assertEqual(Connection.num_pings, 3)
264+
db.close()
265+
db = persist.connection()
266+
self.assert_(not db._con.valid)
267+
self.assertEqual(Connection.num_pings, 3)
268+
cursor = db.cursor()
269+
db._con.close()
270+
self.assert_(not db._con.valid)
271+
self.assertEqual(Connection.num_pings, 3)
272+
cursor.execute('select test')
273+
self.assert_(db._con.valid)
274+
self.assertEqual(Connection.num_pings, 4)
275+
Connection.has_ping = False
276+
Connection.num_pings = 0
277+
226278

227279
if __name__ == '__main__':
228280
unittest.main()

0 commit comments

Comments
 (0)