Skip to content

Commit 2181ffa

Browse files
committed
Fix the semaphore accounting in PooledPg
* close() released the slots of the cached connections a second time. * pooled_connection() leaked a slot when the connection could not be established.
1 parent 46e66be commit 2181ffa

4 files changed

Lines changed: 87 additions & 5 deletions

File tree

dbutils/pooled_pg.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,17 @@ def pooled_connection(self):
224224
if self._connections and not self._connections.acquire(self._blocking):
225225
raise TooManyConnectionsError
226226
try:
227-
return self._cache.get_nowait()
228-
except Empty:
229-
return self.steady_connection()
227+
try:
228+
return self._cache.get_nowait()
229+
except Empty:
230+
return self.steady_connection()
231+
except Exception:
232+
# the connection could not be established, so give back the
233+
# reservation that has been made above (note that this must
234+
# not happen when the reservation itself has already failed)
235+
if self._connections:
236+
self._connections.release()
237+
raise
230238

231239
def connection(self):
232240
"""Get a steady, cached PostgreSQL connection from the pool."""
@@ -248,13 +256,16 @@ def cache(self, con):
248256

249257
def close(self):
250258
"""Close all connections in the pool."""
259+
# Note that the connections in the cache have already given back
260+
# their share of the generally allowed connections when they were
261+
# returned to the pool, so the semaphore must not be released here.
262+
# Connections that are still in use are not closed here, and they
263+
# will release their share as usual by calling the cache() method.
251264
while 1:
252265
try:
253266
con = self._cache.get_nowait()
254267
with suppress(Exception):
255268
con.close()
256-
if self._connections:
257-
self._connections.release()
258269
except Empty:
259270
break
260271

docs/changelog.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ <h2>3.2.0</h2>
4141
connection had already been returned to the pool.</p></li>
4242
<li><p>Fixed <span class="docutils literal">pooled_db.SharedDBConnection</span> violating the contract between
4343
<span class="docutils literal">__eq__</span> and <span class="docutils literal">__hash__</span>; shared connections now compare by identity.</p></li>
44+
<li><p>Fixed <span class="docutils literal">PooledPg.close()</span> releasing the semaphore for the connections in
45+
the pool cache, which had already released it when they were returned to
46+
the pool, so that the pool handed out more connections at the same time
47+
than allowed by <span class="docutils literal">maxconnections</span> after it had been closed.</p></li>
48+
<li><p>Fixed <span class="docutils literal">PooledPg</span> not releasing the semaphore when a connection could not
49+
be established, so that every failed attempt permanently reduced the number
50+
of connections the pool was willing to hand out.</p></li>
4451
</ul>
4552
</section>
4653
<section id="section-2">

docs/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ Bugfixes:
3333
connection had already been returned to the pool.
3434
* Fixed ``pooled_db.SharedDBConnection`` violating the contract between
3535
``__eq__`` and ``__hash__``; shared connections now compare by identity.
36+
* Fixed ``PooledPg.close()`` releasing the semaphore for the connections in
37+
the pool cache, which had already released it when they were returned to
38+
the pool, so that the pool handed out more connections at the same time
39+
than allowed by ``maxconnections`` after it had been closed.
40+
* Fixed ``PooledPg`` not releasing the semaphore when a connection could not
41+
be established, so that every failed attempt permanently reduced the number
42+
of connections the pool was willing to hand out.
3643

3744
3.1.2
3845
=====

tests/test_pooled_pg.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from queue import Empty, Queue
1414
from threading import Thread
1515

16+
import pg
1617
import pytest
1718

1819
from dbutils.pooled_pg import (
@@ -122,6 +123,62 @@ def test_close_connection():
122123
del db
123124

124125

126+
def test_close_all():
127+
"""Check that closing the pool keeps the connections accounted for."""
128+
pool = PooledPg(0, 1, 1, False, None, None, False, 'PooledPgTestDB')
129+
assert pool._cache.qsize() == 0
130+
assert pool._connections._value == 1
131+
db = pool.connection()
132+
db_con = db._con
133+
db.close()
134+
# the cached connection has already given back its share of the
135+
# generally allowed connections when it was returned to the pool
136+
assert pool._cache.qsize() == 1
137+
assert pool._connections._value == 1
138+
pool.close()
139+
# closing the pool discards the cached connection,
140+
# but it must not give back its share a second time
141+
assert pool._cache.qsize() == 0
142+
assert pool._connections._value == 1
143+
assert db_con._closed
144+
# so the pool still hands out only one connection at a time
145+
db = pool.connection()
146+
assert db._con is not db_con
147+
assert pool._connections._value == 0
148+
with pytest.raises(TooManyConnectionsError):
149+
pool.connection()
150+
# closing the pool again (this also happens when it is deleted)
151+
# must not change the accounting while a connection is still in use
152+
pool.close()
153+
assert pool._connections._value == 0
154+
# the connection in use gives back its share when it is returned
155+
db.close()
156+
assert pool._cache.qsize() == 1
157+
assert pool._connections._value == 1
158+
159+
160+
def test_connection_error_does_not_use_up_a_connection():
161+
"""Check that a failing connection attempt is properly accounted for."""
162+
pool = PooledPg(0, 0, 1, False, None, None, False, dbname='ok')
163+
assert pool._cache.qsize() == 0
164+
assert pool._connections._value == 1
165+
# the mock database raises an error when the database is named 'error'
166+
pool._kwargs['dbname'] = 'error'
167+
with pytest.raises(pg.InternalError):
168+
pool.connection()
169+
pool._kwargs['dbname'] = 'ok'
170+
# the failed attempt must not have used up the allowed connection
171+
assert pool._connections._value == 1
172+
db = pool.connection()
173+
assert db.dbname == 'ok'
174+
assert pool._connections._value == 0
175+
# but the connection that could be established still counts
176+
with pytest.raises(TooManyConnectionsError):
177+
pool.connection()
178+
db.close()
179+
assert pool._connections._value == 1
180+
181+
125182
@pytest.mark.parametrize(("mincached", "maxcached", "cached", "rounds"), [
126183
# every round takes the given number of connections out of the pool
127184
# and expects the given number of cached connections after closing them

0 commit comments

Comments
 (0)