Skip to content

Commit 06a2a85

Browse files
committed
Allow checking connections without a ping method
1 parent 5549c9e commit 06a2a85

13 files changed

Lines changed: 577 additions & 47 deletions

dbutils/persistent_db.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,14 @@
4848
really fatal for the connection and the failover mechanism shall
4949
be applied (see the steady_db module for details)
5050
ping: an optional flag controlling when connections are checked
51-
with the ping() method if such a method is available
5251
(0 = None = never, 1 = default = whenever it is requested,
5352
2 = when a cursor is created, 4 = when a query is executed,
5453
7 = always, and all other bit combinations of these values)
54+
By default, connections are checked with the ping() method if
55+
such a method is available. You can also pass an SQL statement
56+
or a callable that shall be used for the check instead, which is
57+
then made as with ping=1, or a tuple with the flag and one of the
58+
latter (see the steady_db module for details).
5559
closeable: if this is set to true, then closing connections will
5660
be allowed, but by default this will be silently ignored
5761
threadlocal: an optional class for representing thread-local data
@@ -173,10 +177,20 @@ def __init__(
173177
database module maps both onto the same exception class. The
174178
callable must tolerate arbitrary exception instances, including
175179
instances without args.
176-
ping: determines when the connection should be checked with ping()
180+
ping: determines when the connection should be checked
177181
(0 = None = never, 1 = default = whenever it is requested,
178182
2 = when a cursor is created, 4 = when a query is executed,
179183
7 = always, and all other bit combinations of these values)
184+
By default, the connection is checked with its ping() method.
185+
Since ping() is not part of the DB-API 2 specification, you
186+
can also pass an SQL statement such as "select 1" that shall
187+
be executed instead, or a callable that is passed the
188+
underlying DB-API 2 connection and shall return whether that
189+
connection is still alive. Such a check is made as with
190+
ping=1, i.e. whenever a connection is requested; if you want
191+
it to be made at other times, pass a tuple with one of the
192+
integer values above and the SQL statement or the callable,
193+
e.g. ping=(4, "select 1").
180194
closeable: if this is set to true, then closing connections will
181195
be allowed, but by default this will be silently ignored
182196
threadlocal: an optional class for representing thread-local data

dbutils/pooled_db.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,14 @@
6363
really fatal for the connection and the failover mechanism shall
6464
be applied (see the steady_db module for details)
6565
ping: an optional flag controlling when connections are checked
66-
with the ping() method if such a method is available
6766
(0 = None = never, 1 = default = whenever fetched from the pool,
6867
2 = when a cursor is created, 4 = when a query is executed,
6968
7 = always, and all other bit combinations of these values)
69+
By default, connections are checked with the ping() method if
70+
such a method is available. You can also pass an SQL statement
71+
or a callable that shall be used for the check instead, which is
72+
then made as with ping=1, or a tuple with the flag and one of the
73+
latter (see the steady_db module for details).
7074
7175
The creator function or the connect function of the DB-API 2 compliant
7276
database module specified as the creator will receive any additional
@@ -239,10 +243,20 @@ def __init__(
239243
database module maps both onto the same exception class. The
240244
callable must tolerate arbitrary exception instances, including
241245
instances without args.
242-
ping: determines when the connection should be checked with ping()
246+
ping: determines when the connection should be checked
243247
(0 = None = never, 1 = default = whenever fetched from the pool,
244248
2 = when a cursor is created, 4 = when a query is executed,
245249
7 = always, and all other bit combinations of these values)
250+
By default, the connection is checked with its ping() method.
251+
Since ping() is not part of the DB-API 2 specification, you
252+
can also pass an SQL statement such as "select 1" that shall
253+
be executed instead, or a callable that is passed the
254+
underlying DB-API 2 connection and shall return whether that
255+
connection is still alive. Such a check is made as with
256+
ping=1, i.e. whenever a connection is fetched from the pool;
257+
if you want it to be made at other times, pass a tuple with
258+
one of the integer values above and the SQL statement or the
259+
callable, e.g. ping=(4, "select 1").
246260
args, kwargs: the parameters that shall be passed to the creator
247261
function or the connection constructor of the DB-API 2 module
248262
"""

dbutils/steady_db.py

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,25 @@ class InvalidCursorError(SteadyDBError):
112112
InvalidCursor = InvalidCursorError
113113

114114

115+
_PING_ERROR = (
116+
"'ping' must be an integer, an SQL statement, a callable,"
117+
" or a tuple with an integer and one of the latter.")
118+
119+
120+
def _sql_pinger(sql):
121+
"""Create a check function executing the given SQL statement."""
122+
def pinger(con):
123+
"""Check the connection by executing an SQL statement."""
124+
cursor = con.cursor()
125+
try:
126+
cursor.execute(sql)
127+
finally:
128+
with suppress(Exception):
129+
cursor.close()
130+
131+
return pinger
132+
133+
115134
def connect(
116135
creator, maxusage=None, setsession=None,
117136
failures=None, ping=1, closeable=True, isfatal=None,
@@ -143,10 +162,21 @@ def connect(
143162
when the database module maps both onto the same exception class.
144163
The callable must tolerate arbitrary exception instances of the
145164
classes given as failures, including instances without args.
146-
ping: determines when the connection should be checked with ping()
165+
ping: determines when and how the connection shall be checked
147166
(0 = None = never, 1 = default = when _ping_check() is called,
148167
2 = whenever a cursor is created, 4 = when a query is executed,
149-
7 = always, and all other bit combinations of these values)
168+
7 = always, and all other bit combinations of these values).
169+
By default, the connection is checked with its ping() method.
170+
Since ping() is not part of the DB-API 2 specification and not
171+
available in every database module, you can also pass an SQL
172+
statement such as "select 1" that shall be executed instead, or
173+
a callable that is passed the underlying DB-API 2 connection and
174+
shall return whether that connection is still alive, where None
175+
counts as alive and raising an exception counts as not alive.
176+
Such a check is made as with ping=1, i.e. when _ping_check()
177+
is called; if you want it to be made at other times, pass a tuple
178+
with one of the integer values above and the SQL statement or the
179+
callable, e.g. ping=(4, "select 1").
150180
closeable: if this is set to false, then closing the connection will
151181
be silently ignored, but by default the connection can be closed
152182
args, kwargs: the parameters that shall be passed to the creator
@@ -212,11 +242,34 @@ def __init__(
212242
if isfatal is not None and not callable(isfatal):
213243
raise TypeError("'isfatal' must be a callable.")
214244
self._isfatal = isfatal
215-
self._ping = ping if isinstance(ping, int) else 0
245+
self._ping, self._pinger = self._parse_ping(ping)
216246
self._closeable = closeable
217247
self._args, self._kwargs = args, kwargs
218248
self._store(self._create())
219249

250+
@staticmethod
251+
def _parse_ping(ping):
252+
"""Split the ping parameter into a flag and a check function.
253+
254+
The ping parameter determines when the connection shall be checked
255+
and can also determine how it shall be checked, by passing an SQL
256+
statement or a callable instead of or together with the flag.
257+
"""
258+
if isinstance(ping, tuple):
259+
try:
260+
ping, pinger = ping
261+
except ValueError:
262+
raise TypeError(_PING_ERROR) from None
263+
elif isinstance(ping, str) or callable(ping):
264+
ping, pinger = 1, ping
265+
else:
266+
pinger = None
267+
if isinstance(pinger, str):
268+
pinger = _sql_pinger(pinger)
269+
elif pinger is not None and not callable(pinger):
270+
raise TypeError(_PING_ERROR)
271+
return ping if isinstance(ping, int) else 0, pinger
272+
220273
def __enter__(self):
221274
"""Enter the runtime context for the connection object."""
222275
return self
@@ -354,29 +407,37 @@ def _reset(self, force=False):
354407
self.rollback()
355408

356409
def _ping_check(self, ping=1, reconnect=True):
357-
"""Check whether the connection is still alive using ping().
410+
"""Check whether the connection is still alive.
411+
412+
The connection is checked with the check function that has been
413+
derived from the ping parameter, or with the ping() method of the
414+
underlying connection if no such function has been specified.
358415
359416
If the underlying connection is not active and the ping
360417
parameter is set accordingly, the connection will be recreated
361418
unless the connection is currently inside a transaction.
362419
"""
363420
if ping & self._ping:
364-
try: # if possible, ping the connection
365-
try: # pass a reconnect=False flag if this is supported
366-
alive = self._con.ping(False)
367-
except TypeError: # the reconnect flag is not supported
368-
alive = self._con.ping()
369-
except (AttributeError, IndexError, TypeError, ValueError):
370-
self._ping = 0 # ping() is not available
371-
alive = None
372-
reconnect = False
373-
except Exception:
374-
alive = False
421+
if self._pinger is None:
422+
try: # if possible, ping the connection
423+
try: # pass a reconnect=False flag if this is supported
424+
alive = self._con.ping(False)
425+
except TypeError: # the reconnect flag is not supported
426+
alive = self._con.ping()
427+
except (AttributeError, IndexError, TypeError, ValueError):
428+
self._ping = 0 # ping() is not available
429+
return None # the connection cannot be checked
430+
except Exception:
431+
alive = False
375432
else:
376-
if alive is None:
377-
alive = True
378-
if alive:
379-
reconnect = False
433+
try: # check the connection with the given check function
434+
alive = self._pinger(self._con)
435+
except Exception:
436+
alive = False
437+
if alive is None:
438+
alive = True
439+
if alive:
440+
reconnect = False
380441
if reconnect and not self._transaction:
381442
try: # try to reopen the connection
382443
con = self._create()

docs/changelog.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ <h2>3.2.0</h2>
2323
failed statement, such as a deliberate server side statement timeout, from
2424
errors that report a broken connection, when the database module maps both
2525
onto the same exception class.</p></li>
26+
<li><p>The <span class="docutils literal">ping</span> parameter of <span class="docutils literal">PersistentDB</span>, <span class="docutils literal">PooledDB</span> and
27+
<span class="docutils literal">steady_db.connect()</span> now also accepts an SQL statement such as
28+
<span class="docutils literal">&quot;select 1&quot;</span> or a callable that shall be used for checking the
29+
connections instead of the <span class="docutils literal">ping()</span> method, which is not part of the
30+
DB-API 2 specification and not provided by every database module. Pass a
31+
tuple with the usual integer value and the SQL statement or the callable
32+
if the connections shall not only be checked when they are requested.</p></li>
2633
<li><p>Added a <span class="docutils literal">no_failover()</span> context manager to steady cursors that suspends
2734
the failover mechanism for individual statements.</p></li>
2835
<li><p>Added <span class="docutils literal">dbapi_connection</span> and <span class="docutils literal">dbapi_cursor</span> attributes providing

docs/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ Changes:
1515
failed statement, such as a deliberate server side statement timeout, from
1616
errors that report a broken connection, when the database module maps both
1717
onto the same exception class.
18+
* The ``ping`` parameter of ``PersistentDB``, ``PooledDB`` and
19+
``steady_db.connect()`` now also accepts an SQL statement such as
20+
``"select 1"`` or a callable that shall be used for checking the
21+
connections instead of the ``ping()`` method, which is not part of the
22+
DB-API 2 specification and not provided by every database module. Pass a
23+
tuple with the usual integer value and the SQL statement or the callable
24+
if the connections shall not only be checked when they are requested.
1825
* Added a ``no_failover()`` context manager to steady cursors that suspends
1926
the failover mechanism for individual statements.
2027
* Added ``dbapi_connection`` and ``dbapi_cursor`` attributes providing

0 commit comments

Comments
 (0)