From 4b13307da908d522f37f4421bae9bbc091fba022 Mon Sep 17 00:00:00 2001 From: Dan Chowdhury Date: Sat, 16 Sep 2017 22:55:00 +0100 Subject: [PATCH 1/2] Cleanup TCP configurations across platforms and unified defaults into one dict --- amqp/platform.py | 32 +++++++++++++----------- amqp/transport.py | 63 +++++++++++++++++++---------------------------- 2 files changed, 43 insertions(+), 52 deletions(-) diff --git a/amqp/platform.py b/amqp/platform.py index 5f2b65f2..94a3537c 100644 --- a/amqp/platform.py +++ b/amqp/platform.py @@ -29,24 +29,30 @@ def _versionatom(s): return int(match.groups()[0]) if match else 0 +# available socket options for TCP level +KNOWN_TCP_OPTS = { + 'TCP_CORK', 'TCP_DEFER_ACCEPT', 'TCP_KEEPCNT', + 'TCP_KEEPIDLE', 'TCP_KEEPINTVL', 'TCP_LINGER2', + 'TCP_MAXSEG', 'TCP_NODELAY', 'TCP_QUICKACK', + 'TCP_SYNCNT', 'TCP_USER_TIMEOUT', 'TCP_WINDOW_CLAMP', +} + LINUX_VERSION = None if sys.platform.startswith('linux'): LINUX_VERSION = _linux_version_to_tuple(platform.release()) + if LINUX_VERSION < (2, 6, 37): + KNOWN_TCP_OPTS.remove('TCP_USER_TIMEOUT') -try: - from socket import TCP_USER_TIMEOUT - HAS_TCP_USER_TIMEOUT = True -except ImportError: # pragma: no cover - # should be in Python 3.6+ on Linux. - TCP_USER_TIMEOUT = 18 - HAS_TCP_USER_TIMEOUT = LINUX_VERSION and LINUX_VERSION >= (2, 6, 37) + # Windows Subsystem for Linux is an edge-case: the Python socket library + # returns most TCP_* enums, but they aren't actually supported + if platform.release().endswith("Microsoft"): + KNOWN_TCP_OPTS = {'TCP_NODELAY', 'TCP_KEEPIDLE', 'TCP_KEEPINTVL', + 'TCP_KEEPCNT'} -HAS_TCP_MAXSEG = True # According to MSDN Windows platforms support getsockopt(TCP_MAXSSEG) but not # setsockopt(TCP_MAXSEG) on IPPROTO_TCP sockets. -if sys.platform.startswith('win'): - HAS_TCP_MAXSEG = False - +elif sys.platform.startswith('win'): + KNOWN_TCP_OPTS = {'TCP_NODELAY'} if sys.version_info < (2, 7, 7): import functools @@ -70,9 +76,7 @@ def _inner(s, *args, **kwargs): __all__ = [ 'LINUX_VERSION', 'SOL_TCP', - 'TCP_USER_TIMEOUT', - 'HAS_TCP_USER_TIMEOUT', - 'HAS_TCP_MAXSEG', + 'KNOWN_TCP_OPTS', 'pack', 'pack_into', 'unpack', diff --git a/amqp/transport.py b/amqp/transport.py index b1af3ae2..36e7cbbf 100644 --- a/amqp/transport.py +++ b/amqp/transport.py @@ -12,7 +12,7 @@ from .exceptions import UnexpectedFrame from .five import items from .platform import ( - SOL_TCP, TCP_USER_TIMEOUT, HAS_TCP_USER_TIMEOUT, HAS_TCP_MAXSEG, + SOL_TCP, KNOWN_TCP_OPTS, pack, unpack, ) from .utils import get_errno, set_cloexec @@ -37,40 +37,14 @@ class SSLError(Exception): # noqa # Match things like: [fe80::1]:5432, from RFC 2732 IPV6_LITERAL = re.compile(r'\[([\.0-9a-f:]+)\](?::(\d+))?') -# available socket options for TCP level -KNOWN_TCP_OPTS = ( - 'TCP_CORK', 'TCP_DEFER_ACCEPT', 'TCP_KEEPCNT', - 'TCP_KEEPIDLE', 'TCP_KEEPINTVL', 'TCP_LINGER2', - 'TCP_NODELAY', 'TCP_QUICKACK', - 'TCP_SYNCNT', 'TCP_WINDOW_CLAMP', -) - -if HAS_TCP_MAXSEG: - KNOWN_TCP_OPTS += ('TCP_MAXSEG',) - -TCP_OPTS = { - getattr(socket, opt) for opt in KNOWN_TCP_OPTS if hasattr(socket, opt) -} DEFAULT_SOCKET_SETTINGS = { - socket.TCP_NODELAY: 1, + 'TCP_NODELAY': 1, + 'TCP_USER_TIMEOUT': 1000, + 'TCP_KEEPIDLE': 60, + 'TCP_KEEPINTVL': 10, + 'TCP_KEEPCNT': 9, } -if HAS_TCP_USER_TIMEOUT: - KNOWN_TCP_OPTS += ('TCP_USER_TIMEOUT',) - TCP_OPTS.add(TCP_USER_TIMEOUT) - DEFAULT_SOCKET_SETTINGS[TCP_USER_TIMEOUT] = 1000 - -try: - from socket import TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT # noqa -except ImportError: - pass -else: - DEFAULT_SOCKET_SETTINGS.update({ - TCP_KEEPIDLE: 60, - TCP_KEEPINTVL: 10, - TCP_KEEPCNT: 9, - }) - def to_host_port(host, default=AMQP_PORT): """Convert hostname:port string to host, port tuple.""" @@ -179,16 +153,29 @@ def _init_socket(self, socket_settings, read_timeout, write_timeout): raise def _get_tcp_socket_defaults(self, sock): - return { - opt: sock.getsockopt(SOL_TCP, opt) for opt in TCP_OPTS - } + tcp_opts = {} + for opt in KNOWN_TCP_OPTS: + enum = None + if opt == 'TCP_USER_TIMEOUT': + try: + from socket import TCP_USER_TIMEOUT as enum + except ImportError: + # should be in Python 3.6+ on Linux. + enum = 18 + elif hasattr(socket, opt): + enum = getattr(socket, opt) + + if enum: + if opt in DEFAULT_SOCKET_SETTINGS: + tcp_opts[enum] = DEFAULT_SOCKET_SETTINGS[opt] + else: + tcp_opts[enum] = sock.getsockopt(SOL_TCP, opt) + return tcp_opts def _set_socket_options(self, socket_settings): tcp_opts = self._get_tcp_socket_defaults(self.sock) - final_socket_settings = dict(DEFAULT_SOCKET_SETTINGS) if socket_settings: - final_socket_settings.update(socket_settings) - tcp_opts.update(final_socket_settings) + tcp_opts.update(socket_settings) for opt, val in items(tcp_opts): self.sock.setsockopt(SOL_TCP, opt, val) From a98c9608f42685cc5d27ee513294ea3ed1e40195 Mon Sep 17 00:00:00 2001 From: darkdreamingdan Date: Sun, 15 Oct 2017 03:17:36 +0100 Subject: [PATCH 2/2] Added tests for platforms --- t/unit/test_platform.py | 48 ++++++++++++++++++++++++++++++++++++++++ t/unit/test_transport.py | 7 ++++++ 2 files changed, 55 insertions(+) diff --git a/t/unit/test_platform.py b/t/unit/test_platform.py index 96bc3ac0..fb48d641 100644 --- a/t/unit/test_platform.py +++ b/t/unit/test_platform.py @@ -1,8 +1,21 @@ from __future__ import absolute_import, unicode_literals + +import itertools +import operator + import pytest + from amqp.platform import _linux_version_to_tuple +def reload_module(module): + try: + import importlib + importlib.reload(module) + except Exception: + reload(module) + + def test_struct_argument_type(): from amqp.exceptions import FrameSyntaxError FrameSyntaxError() @@ -14,6 +27,41 @@ def test_struct_argument_type(): ('4.4.34+', (4, 4, 34)), ('4.4.what', (4, 4, 0)), ('4.what.what', (4, 0, 0)), + ('4.4.0-43-Microsoft', (4, 4, 0)), ]) def test_linux_version_to_tuple(s, expected): assert _linux_version_to_tuple(s) == expected + + +def monkeypatch_platform(monkeypatch, sys_platform, platform_release): + monkeypatch.setattr("sys.platform", sys_platform) + + def release(): + return platform_release + + monkeypatch.setattr("platform.release", release) + + +def test_tcp_opts_change(monkeypatch): + monkeypatch_platform(monkeypatch, 'linux', '2.6.36-1-amd64') + + import amqp.platform + reload_module(amqp.platform) + old_linux = amqp.platform.KNOWN_TCP_OPTS + + monkeypatch_platform(monkeypatch, 'linux', '2.6.37-0-41-generic') + reload_module(amqp.platform) + new_linux = amqp.platform.KNOWN_TCP_OPTS + + monkeypatch_platform(monkeypatch, 'win32', '7') + reload_module(amqp.platform) + win = amqp.platform.KNOWN_TCP_OPTS + + monkeypatch_platform(monkeypatch, 'linux', '4.4.0-43-Microsoft') + reload_module(amqp.platform) + win_bash = amqp.platform.KNOWN_TCP_OPTS + + li = [old_linux, new_linux, win, win_bash] + assert all(operator.ne(*i) for i in itertools.combinations(li, 2)) + + assert len(win) <= len(win_bash) < len(old_linux) < len(new_linux) diff --git a/t/unit/test_transport.py b/t/unit/test_transport.py index 2eedc334..b213ac18 100644 --- a/t/unit/test_transport.py +++ b/t/unit/test_transport.py @@ -9,6 +9,7 @@ from amqp import transport from amqp.exceptions import UnexpectedFrame from amqp.platform import pack +from amqp.transport import _AbstractTransport class MockSocket(object): @@ -176,6 +177,12 @@ def test_passing_tcp_nodelay(self): result = self.socket.getsockopt(socket.SOL_TCP, socket.TCP_NODELAY) assert result == expected + def test_platform_socket_opts(self): + s = socket.socket() + opts = _AbstractTransport(self.host)._get_tcp_socket_defaults(s) + + assert opts + class test_AbstractTransport: