diff --git a/doc/changelog.rst b/doc/changelog.rst index 6e3454de1..ebd6cf4da 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -26,6 +26,10 @@ Changes and Deprecations: Applications should use :meth:`~pymongo.mongo_client.MongoClient.get_database` without the `name` parameter instead. +- Deprecated the MongoClient option `socketKeepAlive`. It now defaults to true + and disabling it is not recommended, see `does TCP keepalive time affect + MongoDB Deployments? + `_ - If a custom :class:`~bson.codec_options.CodecOptions` is passed to :class:`RawBSONDocument`, its `document_class` must be :class:`RawBSONDocument`. diff --git a/pymongo/client_options.py b/pymongo/client_options.py index 739a24098..ad71a9613 100644 --- a/pymongo/client_options.py +++ b/pymongo/client_options.py @@ -106,7 +106,7 @@ def _parse_pool_options(options): if max_pool_size is not None and min_pool_size > max_pool_size: raise ValueError("minPoolSize must be smaller or equal to maxPoolSize") connect_timeout = options.get('connecttimeoutms', common.CONNECT_TIMEOUT) - socket_keepalive = options.get('socketkeepalive', False) + socket_keepalive = options.get('socketkeepalive', True) socket_timeout = options.get('sockettimeoutms') wait_queue_timeout = options.get('waitqueuetimeoutms') wait_queue_multiple = options.get('waitqueuemultiple') diff --git a/pymongo/mongo_client.py b/pymongo/mongo_client.py index ce7852c01..62977b829 100644 --- a/pymongo/mongo_client.py +++ b/pymongo/mongo_client.py @@ -196,9 +196,6 @@ class MongoClient(common.BaseObject): - `waitQueueMultiple`: (integer or None) Multiplied by maxPoolSize to give the number of threads allowed to wait for a socket at one time. Defaults to ``None`` (no limit). - - `socketKeepAlive`: (boolean) Whether to send periodic keep-alive - packets on connected sockets. Defaults to ``False`` (do not send - keep-alive packets). - `heartbeatFrequencyMS`: (optional) The number of milliseconds between periodic server checks, or None to accept the default frequency of 10 seconds. @@ -209,6 +206,10 @@ class MongoClient(common.BaseObject): profile collections. - `event_listeners`: a list or tuple of event listeners. See :mod:`~pymongo.monitoring` for details. + - `socketKeepAlive`: (boolean) **DEPRECATED** Whether to send + periodic keep-alive packets on connected sockets. Defaults to + ``True``. Disabling it is not recommended, see + https://docs.mongodb.com/manual/faq/diagnostics/#does-tcp-keepalive-time-affect-mongodb-deployments", | **Write Concern options:** | (Only set if passed. No default values.) @@ -340,6 +341,8 @@ class MongoClient(common.BaseObject): Add ``username`` and ``password`` options. Document the ``authSource``, ``authMechanism``, and ``authMechanismProperties `` options. + Deprecated the `socketKeepAlive` keyword argument and URI option. + `socketKeepAlive` now defaults to ``True``. .. versionchanged:: 3.0 :class:`~pymongo.mongo_client.MongoClient` is now the one and only @@ -453,6 +456,13 @@ class MongoClient(common.BaseObject): # Username and password passed as kwargs override user info in URI. username = opts.get("username", username) password = opts.get("password", password) + if 'socketkeepalive' in opts: + warnings.warn( + "The socketKeepAlive option is deprecated. It now" + "defaults to true and disabling it is not recommended, see " + "https://docs.mongodb.com/manual/faq/diagnostics/" + "#does-tcp-keepalive-time-affect-mongodb-deployments", + DeprecationWarning, stacklevel=2) self.__options = options = ClientOptions( username, password, dbase, opts) diff --git a/pymongo/pool.py b/pymongo/pool.py index 111dc6028..a46b1bef7 100644 --- a/pymongo/pool.py +++ b/pymongo/pool.py @@ -30,7 +30,7 @@ except ImportError: from bson import DEFAULT_CODEC_OPTIONS -from bson.py3compat import imap, itervalues, _unicode +from bson.py3compat import imap, itervalues, _unicode, integer_types from bson.son import SON from pymongo import auth, helpers, thread_util, __version__ from pymongo.common import MAX_MESSAGE_SIZE @@ -111,6 +111,53 @@ except ImportError: """Dummy function for platforms that don't provide fcntl.""" pass +_MAX_TCP_KEEPIDLE = 300 +_MAX_TCP_KEEPINTVL = 10 +_MAX_TCP_KEEPCNT = 9 + +if sys.platform == 'win32': + try: + import _winreg as winreg + except ImportError: + import winreg + + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters") as key: + _DEFAULT_TCP_IDLE_MS, _ = winreg.QueryValueEx(key, "KeepAliveTime") + _DEFAULT_TCP_INTERVAL_MS, _ = winreg.QueryValueEx( + key, "KeepAliveInterval") + # Make sure these are integers. + if not isinstance(_DEFAULT_TCP_IDLE_MS, integer_types): + raise ValueError + if not isinstance(_DEFAULT_TCP_INTERVAL_MS, integer_types): + raise ValueError + except (OSError, ValueError): + # We could not check the default values so do not attempt to override. + def _set_keepalive_times(dummy): + pass + else: + def _set_keepalive_times(sock): + idle_ms = min(_DEFAULT_TCP_IDLE_MS, _MAX_TCP_KEEPIDLE * 1000) + interval_ms = min(_DEFAULT_TCP_INTERVAL_MS, + _MAX_TCP_KEEPINTVL * 1000) + if (idle_ms < _DEFAULT_TCP_IDLE_MS or + interval_ms < _DEFAULT_TCP_INTERVAL_MS): + sock.ioctl(socket.SIO_KEEPALIVE_VALS, + (1, idle_ms, interval_ms)) +else: + def _set_tcp_option(sock, tcp_option, max_value): + if hasattr(socket, tcp_option): + sockopt = getattr(socket, tcp_option) + default = sock.getsockopt(socket.SOL_TCP, sockopt) + if default > max_value: + sock.setsockopt(socket.SOL_TCP, sockopt, max_value) + + def _set_keepalive_times(sock): + _set_tcp_option(sock, 'TCP_KEEPIDLE', _MAX_TCP_KEEPIDLE) + _set_tcp_option(sock, 'TCP_KEEPINTVL', _MAX_TCP_KEEPINTVL) + _set_tcp_option(sock, 'TCP_KEEPCNT', _MAX_TCP_KEEPCNT) _METADATA = SON([ ('driver', SON([('name', 'PyMongo'), ('version', __version__)])), @@ -223,7 +270,7 @@ class PoolOptions(object): max_idle_time_ms=None, connect_timeout=None, socket_timeout=None, wait_queue_timeout=None, wait_queue_multiple=None, ssl_context=None, - ssl_match_hostname=True, socket_keepalive=False, + ssl_match_hostname=True, socket_keepalive=True, event_listeners=None, appname=None): self.__max_pool_size = max_pool_size @@ -619,6 +666,8 @@ def _create_connection(address, options): sock.settimeout(options.connect_timeout) sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, options.socket_keepalive) + if options.socket_keepalive: + _set_keepalive_times(sock) sock.connect(sa) return sock except socket.error as e: diff --git a/pymongo/topology.py b/pymongo/topology.py index deb7b7008..f7e645e0c 100644 --- a/pymongo/topology.py +++ b/pymongo/topology.py @@ -463,7 +463,6 @@ class Topology(object): socket_timeout=options.connect_timeout, ssl_context=options.ssl_context, ssl_match_hostname=options.ssl_match_hostname, - socket_keepalive=True, event_listeners=options.event_listeners, appname=options.appname) diff --git a/test/test_client.py b/test/test_client.py index da8f96f23..4e83fd3a9 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -23,7 +23,7 @@ import socket import struct import sys import time -import traceback +import warnings sys.path[0:0] = [""] @@ -94,7 +94,6 @@ class ClientUnitTest(unittest.TestCase): connectTimeoutMS=20000, waitQueueTimeoutMS=None, waitQueueMultiple=None, - socketKeepAlive=False, replicaSet=None, read_preference=ReadPreference.PRIMARY, ssl=False, @@ -112,7 +111,7 @@ class ClientUnitTest(unittest.TestCase): self.assertEqual(20.0, pool_opts.connect_timeout) self.assertEqual(None, pool_opts.wait_queue_timeout) self.assertEqual(None, pool_opts.wait_queue_multiple) - self.assertFalse(pool_opts.socket_keepalive) + self.assertTrue(pool_opts.socket_keepalive) self.assertEqual(None, pool_opts.ssl_context) self.assertEqual(None, options.replica_set_name) self.assertEqual(ReadPreference.PRIMARY, client.read_preference) @@ -777,8 +776,19 @@ class TestClient(IntegrationTest): self.assertEqual(pool._socket_semaphore.waiter_semaphore.counter, 6) def test_socketKeepAlive(self): - client = rs_or_single_client(socketKeepAlive=True) - self.assertTrue(get_pool(client).opts.socket_keepalive) + for socketKeepAlive in [True, False]: + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter("always") + client = rs_or_single_client(socketKeepAlive=socketKeepAlive) + self.assertIn("The socketKeepAlive option is deprecated", + str(ctx[0])) + pool = get_pool(client) + self.assertEqual(socketKeepAlive, + pool.opts.socket_keepalive) + with pool.get_socket({}) as sock_info: + keepalive = sock_info.sock.getsockopt(socket.SOL_SOCKET, + socket.SO_KEEPALIVE) + self.assertEqual(socketKeepAlive, bool(keepalive)) def test_tz_aware(self): self.assertRaises(ValueError, MongoClient, tz_aware='foo')