Skip to content

Commit 1060814

Browse files
committed
PYTHON-658 - Support minPoolSize, maxIdleTimeMS
1 parent ecab1c9 commit 1060814

File tree

10 files changed

+228
-56
lines changed

10 files changed

+228
-56
lines changed

doc/faq.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ all other sockets are in use and the pool has reached its maximum, the
2828
thread pauses, waiting for a socket to be returned to the pool by another
2929
thread.
3030

31+
It is possible to set the minimum number of concurrent connections to each
32+
server with ``minPoolSize``, which defaults to 0. The connection pool will be
33+
initialized with this number of sockets. If sockets are removed from the pool
34+
and closed, causing the total number of sockets (both in use and idle) to drop
35+
below the set minimum, more sockets will be added until the minimum is reached.
36+
37+
The maximum number of milliseconds that a connection can remain idle in the
38+
pool before being removed and replaced can be set with ``maxIdleTime``, which
39+
defaults to `None` (no limit).
40+
3141
The default configuration for a :class:`~pymongo.mongo_client.MongoClient`
3242
works for most applications::
3343

pymongo/client_options.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ def _parse_ssl_options(options):
9898
def _parse_pool_options(options):
9999
"""Parse connection pool options."""
100100
max_pool_size = options.get('maxpoolsize', common.MAX_POOL_SIZE)
101+
min_pool_size = options.get('minpoolsize', common.MIN_POOL_SIZE)
102+
max_idle_time_ms = options.get('maxidletimems', common.MAX_IDLE_TIME_MS)
103+
if max_pool_size is not None and min_pool_size > max_pool_size:
104+
raise ValueError("minPoolSize must be smaller or equal to maxPoolSize")
101105
connect_timeout = options.get('connecttimeoutms', common.CONNECT_TIMEOUT)
102106
socket_keepalive = options.get('socketkeepalive', False)
103107
socket_timeout = options.get('sockettimeoutms')
@@ -106,6 +110,8 @@ def _parse_pool_options(options):
106110
event_listeners = options.get('event_listeners')
107111
ssl_context, ssl_match_hostname = _parse_ssl_options(options)
108112
return PoolOptions(max_pool_size,
113+
min_pool_size,
114+
max_idle_time_ms,
109115
connect_timeout, socket_timeout,
110116
wait_queue_timeout, wait_queue_multiple,
111117
ssl_context, ssl_match_hostname, socket_keepalive,

pymongo/common.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@
6464
# Default value for maxPoolSize.
6565
MAX_POOL_SIZE = 100
6666

67+
# Default value for minPoolSize.
68+
MIN_POOL_SIZE = 0
69+
70+
# Default value for maxIdleTimeMS.
71+
MAX_IDLE_TIME_MS = None
72+
6773
# Default value for localThresholdMS.
6874
LOCAL_THRESHOLD_MS = 15
6975

@@ -441,13 +447,16 @@ def validate_ok_for_update(update):
441447
'tz_aware': validate_boolean_or_string,
442448
'uuidrepresentation': validate_uuid_representation,
443449
'connect': validate_boolean_or_string,
450+
'event_listeners': _validate_event_listeners,
451+
'minpoolsize': validate_non_negative_integer
444452
}
445453

446454
TIMEOUT_VALIDATORS = {
447455
'connecttimeoutms': validate_timeout_or_none,
448456
'sockettimeoutms': validate_timeout_or_none,
449457
'waitqueuetimeoutms': validate_timeout_or_none,
450458
'serverselectiontimeoutms': validate_timeout_or_zero,
459+
'maxidletimems': validate_timeout_or_none,
451460
}
452461

453462
KW_VALIDATORS = {

pymongo/helpers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import collections
1818
import datetime
1919
import struct
20+
import sys
21+
import traceback
2022

2123
import bson
2224
from bson.codec_options import CodecOptions
@@ -337,3 +339,20 @@ def _fields_list_to_dict(fields, option_name):
337339

338340
raise TypeError("%s must be a mapping or "
339341
"list of key names" % (option_name,))
342+
343+
344+
def _handle_exception():
345+
"""Print exceptions raised by subscribers to stderr."""
346+
# Heavily influenced by logging.Handler.handleError.
347+
348+
# See note here:
349+
# https://docs.python.org/3.4/library/sys.html#sys.__stderr__
350+
if sys.stderr:
351+
einfo = sys.exc_info()
352+
try:
353+
traceback.print_exception(einfo[0], einfo[1], einfo[2],
354+
None, sys.stderr)
355+
except IOError:
356+
pass
357+
finally:
358+
del einfo

pymongo/mongo_client.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import contextlib
3535
import datetime
3636
import threading
37-
import warnings
3837
import weakref
3938
from collections import defaultdict
4039

@@ -124,10 +123,16 @@ def __init__(
124123
125124
| **Other optional parameters can be passed as keyword arguments:**
126125
127-
- `maxPoolSize` (optional): The maximum number of connections
128-
that the pool will open simultaneously. If this is set, operations
129-
will block if there are `maxPoolSize` outstanding connections
130-
from the pool. Defaults to 100. Cannot be 0.
126+
- `maxPoolSize` (optional): The maximum allowable number of
127+
concurrent connections to each connected server. Requests to a
128+
server will block if there are `maxPoolSize` outstanding
129+
connections to the requested server. Defaults to 100. Cannot be 0.
130+
- `minPoolSize` (optional): The minimum required number of concurrent
131+
connections that the pool will maintain to each connected server.
132+
Default is 0.
133+
- `maxIdleTimeMS` (optional): The maximum number of milliseconds that
134+
a connection can remain idle in the pool before being removed and
135+
replaced. Defaults to `None` (no limit).
131136
- `socketTimeoutMS`: (integer or None) Controls how long (in
132137
milliseconds) the driver will wait for a response after sending an
133138
ordinary (non-monitoring) database operation before concluding that
@@ -390,7 +395,7 @@ def target():
390395
client = self_ref()
391396
if client is None:
392397
return False # Stop the executor.
393-
MongoClient._process_kill_cursors_queue(client)
398+
MongoClient._process_periodic_tasks(client)
394399
return True
395400

396401
executor = periodic_executor.PeriodicExecutor(
@@ -598,15 +603,34 @@ def is_mongos(self):
598603

599604
@property
600605
def max_pool_size(self):
601-
"""The maximum number of sockets the pool will open concurrently.
602-
603-
When the pool has reached `max_pool_size`, operations block waiting for
604-
a socket to be returned to the pool. If ``waitQueueTimeoutMS`` is set,
605-
a blocked operation will raise :exc:`~pymongo.errors.ConnectionFailure`
606-
after a timeout. By default ``waitQueueTimeoutMS`` is not set.
606+
"""The maximum allowable number of concurrent connections to each
607+
connected server. Requests to a server will block if there are
608+
`maxPoolSize` outstanding connections to the requested server.
609+
Defaults to 100. Cannot be 0.
610+
611+
When a server's pool has reached `max_pool_size`, operations for that
612+
server block waiting for a socket to be returned to the pool. If
613+
``waitQueueTimeoutMS`` is set, a blocked operation will raise
614+
:exc:`~pymongo.errors.ConnectionFailure` after a timeout.
615+
By default ``waitQueueTimeoutMS`` is not set.
607616
"""
608617
return self.__options.pool_options.max_pool_size
609618

619+
@property
620+
def min_pool_size(self):
621+
"""The minimum required number of concurrent connections that the pool
622+
will maintain to each connected server. Default is 0.
623+
"""
624+
return self.__options.pool_options.min_pool_size
625+
626+
@property
627+
def max_idle_time_ms(self):
628+
"""The maximum number of milliseconds that a connection can remain
629+
idle in the pool before being removed and replaced. Defaults to
630+
`None` (no limit).
631+
"""
632+
return self.__options.pool_options.max_idle_time_ms
633+
610634
@property
611635
def nodes(self):
612636
"""Set of all currently connected servers.
@@ -945,8 +969,9 @@ def kill_cursors(self, cursor_ids, address=None):
945969
self.__kill_cursors_queue.append((address, cursor_ids))
946970

947971
# This method is run periodically by a background thread.
948-
def _process_kill_cursors_queue(self):
949-
"""Process any pending kill cursors requests."""
972+
def _process_periodic_tasks(self):
973+
"""Process any pending kill cursors requests and
974+
maintain connection pool parameters."""
950975
address_to_cursor_ids = defaultdict(list)
951976

952977
# Other threads or the GC may append to the queue concurrently.
@@ -1018,9 +1043,12 @@ def _process_kill_cursors_queue(self):
10181043
duration, reply, 'killCursors', request_id,
10191044
address)
10201045

1021-
except ConnectionFailure as exc:
1022-
warnings.warn("couldn't close cursor on %s: %s"
1023-
% (address, exc))
1046+
except Exception:
1047+
helpers._handle_exception()
1048+
try:
1049+
self._topology.update_pool()
1050+
except Exception:
1051+
helpers._handle_exception()
10241052

10251053
def server_info(self):
10261054
"""Get information about the MongoDB server we're connected to."""

pymongo/monitoring.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def failed(self, event):
7070
import traceback
7171

7272
from collections import namedtuple, Sequence
73+
from pymongo.helpers import _handle_exception
7374

7475
_Listeners = namedtuple('Listeners', ('command_listeners',))
7576

@@ -133,22 +134,6 @@ def register(listener):
133134
_LISTENERS.command_listeners.append(listener)
134135

135136

136-
def _handle_exception():
137-
"""Print exceptions raised by subscribers to stderr."""
138-
# Heavily influenced by logging.Handler.handleError.
139-
140-
# See note here:
141-
# https://docs.python.org/3.4/library/sys.html#sys.__stderr__
142-
if sys.stderr:
143-
einfo = sys.exc_info()
144-
try:
145-
traceback.print_exception(einfo[0], einfo[1], einfo[2],
146-
None, sys.stderr)
147-
except IOError:
148-
pass
149-
finally:
150-
del einfo
151-
152137
# Note - to avoid bugs from forgetting which if these is all lowercase and
153138
# which are camelCase, and at the same time avoid having to add a test for
154139
# every command, use all lowercase here and test against command_name.lower().

pymongo/pool.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,22 @@ def _raise_connection_failure(address, error):
6767

6868
class PoolOptions(object):
6969

70-
__slots__ = ('__max_pool_size', '__connect_timeout', '__socket_timeout',
70+
__slots__ = ('__max_pool_size', '__min_pool_size', '__max_idle_time_ms',
71+
'__connect_timeout', '__socket_timeout',
7172
'__wait_queue_timeout', '__wait_queue_multiple',
7273
'__ssl_context', '__ssl_match_hostname', '__socket_keepalive',
7374
'__event_listeners')
7475

75-
def __init__(self, max_pool_size=100, connect_timeout=None,
76+
def __init__(self, max_pool_size=100, min_pool_size=0,
77+
max_idle_time_ms=None, connect_timeout=None,
7678
socket_timeout=None, wait_queue_timeout=None,
7779
wait_queue_multiple=None, ssl_context=None,
7880
ssl_match_hostname=True, socket_keepalive=False,
7981
event_listeners=None):
8082

8183
self.__max_pool_size = max_pool_size
84+
self.__min_pool_size = min_pool_size
85+
self.__max_idle_time_ms = max_idle_time_ms
8286
self.__connect_timeout = connect_timeout
8387
self.__socket_timeout = socket_timeout
8488
self.__wait_queue_timeout = wait_queue_timeout
@@ -90,12 +94,34 @@ def __init__(self, max_pool_size=100, connect_timeout=None,
9094

9195
@property
9296
def max_pool_size(self):
93-
"""The maximum number of connections that the pool will open
94-
simultaneously. If this is set, operations will block if there
95-
are `max_pool_size` outstanding connections.
97+
"""The maximum allowable number of concurrent connections to each
98+
connected server. Requests to a server will block if there are
99+
`maxPoolSize` outstanding connections to the requested server.
100+
Defaults to 100. Cannot be 0.
101+
102+
When a server's pool has reached `max_pool_size`, operations for that
103+
server block waiting for a socket to be returned to the pool. If
104+
``waitQueueTimeoutMS`` is set, a blocked operation will raise
105+
:exc:`~pymongo.errors.ConnectionFailure` after a timeout.
106+
By default ``waitQueueTimeoutMS`` is not set.
96107
"""
97108
return self.__max_pool_size
98109

110+
@property
111+
def min_pool_size(self):
112+
"""The minimum required number of concurrent connections that the pool
113+
will maintain to each connected server. Default is 0.
114+
"""
115+
return self.__min_pool_size
116+
117+
@property
118+
def max_idle_time_ms(self):
119+
"""The maximum number of milliseconds that a connection can remain
120+
idle in the pool before being removed and replaced. Defaults to
121+
`None` (no limit).
122+
"""
123+
return self.__max_idle_time_ms
124+
99125
@property
100126
def connect_timeout(self):
101127
"""How long a connection can take to be opened before timing out.
@@ -459,6 +485,7 @@ def __init__(self, address, options, handshake=True):
459485

460486
self.sockets = set()
461487
self.lock = threading.Lock()
488+
self.active_sockets = 0
462489

463490
# Keep track of resets, so we notice sockets created before the most
464491
# recent reset and close them.
@@ -483,10 +510,26 @@ def reset(self):
483510
self.pool_id += 1
484511
self.pid = os.getpid()
485512
sockets, self.sockets = self.sockets, set()
513+
self.active_sockets = 0
486514

487515
for sock_info in sockets:
488516
sock_info.close()
489517

518+
def remove_stale_sockets(self):
519+
with self.lock:
520+
if self.opts.max_idle_time_ms is not None:
521+
for sock_info in self.sockets.copy():
522+
age = _time() - sock_info.last_checkout
523+
if age > self.opts.max_idle_time_ms:
524+
self.sockets.remove(sock_info)
525+
sock_info.close()
526+
527+
while len(
528+
self.sockets) + self.active_sockets < self.opts.min_pool_size:
529+
sock_info = self.connect()
530+
with self.lock:
531+
self.sockets.add(sock_info)
532+
490533
def connect(self):
491534
"""Connect to Mongo and return a new SocketInfo.
492535
@@ -560,6 +603,8 @@ def _get_socket_no_auth(self):
560603
if not self._socket_semaphore.acquire(
561604
True, self.opts.wait_queue_timeout):
562605
self._raise_wait_queue_timeout()
606+
with self.lock:
607+
self.active_sockets += 1
563608

564609
# We've now acquired the semaphore and must release it on error.
565610
try:
@@ -571,13 +616,21 @@ def _get_socket_no_auth(self):
571616
except KeyError:
572617
# Can raise ConnectionFailure or CertificateError.
573618
sock_info, from_pool = self.connect(), False
619+
# If socket is idle, open a new one.
620+
if self.opts.max_idle_time_ms is not None:
621+
age = _time() - sock_info.last_checkout
622+
if age > self.opts.max_idle_time_ms:
623+
sock_info.close()
624+
sock_info, from_pool = self.connect(), False
574625

575626
if from_pool:
576627
# Can raise ConnectionFailure.
577628
sock_info = self._check(sock_info)
578629

579630
except:
580631
self._socket_semaphore.release()
632+
with self.lock:
633+
self.active_sockets -= 1
581634
raise
582635

583636
sock_info.last_checkout = _time()
@@ -595,6 +648,8 @@ def return_socket(self, sock_info):
595648
self.sockets.add(sock_info)
596649

597650
self._socket_semaphore.release()
651+
with self.lock:
652+
self.active_sockets -= 1
598653

599654
def _check(self, sock_info):
600655
"""This side-effecty function checks if this pool has been reset since

pymongo/topology.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@ def reset_server_and_request_check(self, address):
254254
self._reset_server(address)
255255
self._request_check(address)
256256

257+
def update_pool(self):
258+
# Remove any stale sockets and add new sockets if pool is too small.
259+
with self._lock:
260+
for server in self._servers.values():
261+
server._pool.remove_stale_sockets()
262+
257263
def close(self):
258264
"""Clear pools and terminate monitors. Topology reopens on demand."""
259265
with self._lock:

test/pymongo_mocks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def mock_is_master(self, host):
192192

193193
return response, rtt
194194

195-
def _process_kill_cursors_queue(self):
195+
def _process_periodic_tasks(self):
196196
# Avoid the background thread causing races, e.g. a surprising
197197
# reconnect while we're trying to test a disconnected client.
198198
pass

0 commit comments

Comments
 (0)