@@ -67,18 +67,22 @@ def _raise_connection_failure(address, error):
6767
6868class 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
0 commit comments