Skip to content

Commit 5a1b60f

Browse files
Add flag to enable TCP keepalives (#163)
1 parent cb22031 commit 5a1b60f

File tree

7 files changed

+135
-21
lines changed

7 files changed

+135
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## Local development
88

9-
A light-weight HTTP client library.
9+
A light-weight HTTP client library for conjure.
1010

1111
### Overview
1212

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type: improvement
2+
improvement:
3+
description: Add flag to enable TCP keepalives
4+
links:
5+
- https://github.com/palantir/conjure-python-client/pull/161

conjure_python_client/_http/requests_client.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from requests.adapters import HTTPAdapter, Response, CaseInsensitiveDict
1616
from typing import TypeVar, Type, List, Optional, Dict, Any, Union
1717
from requests.exceptions import HTTPError
18+
from requests.packages.urllib3.connection import HTTPConnection
1819
from requests.packages.urllib3.poolmanager import PoolManager
1920
from requests.packages.urllib3.util.ssl_ import create_urllib3_context
2021
from requests.packages.urllib3.util import Retry
@@ -24,6 +25,8 @@
2425
import os
2526
import random
2627
import requests
28+
import socket
29+
import sys
2730

2831

2932
T = TypeVar("T")
@@ -47,6 +50,24 @@
4750
"DHE-RSA-AES256-GCM-SHA384"
4851
)
4952

53+
SOCKET_KEEP_ALIVE = (
54+
socket.SOL_SOCKET,
55+
socket.SO_KEEPALIVE,
56+
1,
57+
) # Enable keep alive.
58+
SOCKET_KEEP_INTVL = (
59+
socket.SOL_TCP,
60+
socket.TCP_KEEPINTVL,
61+
120,
62+
) # Interval of 120s between individual keepalive probes.
63+
KEEP_ALIVE_SOCKET_OPTIONS = [SOCKET_KEEP_ALIVE, SOCKET_KEEP_INTVL]
64+
if sys.platform != "darwin":
65+
SOCKET_KEEP_IDLE = (
66+
socket.SOL_TCP,
67+
socket.TCP_KEEPIDLE,
68+
120,
69+
) # After 120s of idle connection, start keepalive probes.
70+
KEEP_ALIVE_SOCKET_OPTIONS.append(SOCKET_KEEP_IDLE)
5071

5172
TRACE_ID_HEADER: str = "X-B3-TraceId"
5273
TRACE_ID_RANDOM_BYTES = 8
@@ -164,6 +185,7 @@ def create(
164185
user_agent: str,
165186
service_config: ServiceConfiguration,
166187
return_none_for_unknown_union_types: bool = False,
188+
enable_keep_alive: bool = False,
167189
) -> T:
168190
# setup retry to match java remoting
169191
# https://github.com/palantir/http-remoting/tree/3.12.0#quality-of-service-retry-failover-throttling
@@ -173,7 +195,9 @@ def create(
173195
status_forcelist=[308, 429, 503],
174196
backoff_factor=float(service_config.backoff_slot_size) / 1000,
175197
)
176-
transport_adapter = TransportAdapter(max_retries=retry)
198+
transport_adapter = TransportAdapter(
199+
max_retries=retry, enable_keep_alive=enable_keep_alive
200+
)
177201
# create a session, for shared connection polling, user agent, etc
178202
session = requests.Session()
179203
session.headers = CaseInsensitiveDict({"User-Agent": user_agent})
@@ -196,18 +220,29 @@ def create(
196220
class TransportAdapter(HTTPAdapter):
197221
"""Transport adapter that allows customising ssl things"""
198222

223+
__attrs__ = HTTPAdapter.__attrs__ + ["_enable_keep_alive"]
224+
225+
def __init__(self, *args, enable_keep_alive: bool = False, **kwargs):
226+
self._enable_keep_alive = enable_keep_alive
227+
super().__init__(*args, **kwargs)
228+
199229
def init_poolmanager(
200230
self, connections, maxsize, block=False, **pool_kwargs
201231
):
202232
self._pool_connections = connections
203233
self._pool_maxsize = maxsize
204234
self._pool_block = block
205235
ssl_context = create_urllib3_context(ciphers=CIPHERS)
236+
keep_alive_pool_kwargs = {
237+
"socket_options": HTTPConnection.default_socket_options
238+
+ KEEP_ALIVE_SOCKET_OPTIONS
239+
}
240+
if self._enable_keep_alive:
241+
pool_kwargs = {**pool_kwargs, **keep_alive_pool_kwargs}
206242
self.poolmanager = PoolManager(
207243
num_pools=connections,
208244
maxsize=maxsize,
209245
block=block,
210-
strict=True,
211246
ssl_context=ssl_context,
212247
**pool_kwargs
213248
)

conjure_python_client/_serde/decoder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ def decode_conjure_union_type(
141141
# for backwards compatibility with conjure-python,
142142
# only pass in arg type_of_union if it is expected
143143
param_dict = inspect.signature(conjure_type.__init__).parameters
144-
if 'type_of_union' in param_dict:
145-
deserialized['type_of_union'] = type_of_union
144+
if "type_of_union" in param_dict:
145+
deserialized["type_of_union"] = type_of_union
146146
return conjure_type(**deserialized)
147147

148148
@classmethod

setup.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,63 @@
1414
#!/usr/bin/env python
1515
from setuptools import find_packages, setup, Command
1616
from os import path, makedirs, system
17+
import re
1718
import subprocess
1819
import sys
1920

2021
VERSION_PY_PATH = "conjure_python_client/_version.py"
2122

23+
GIT_REGEX_PATTERN = re.compile(
24+
r"^"
25+
+ r"(?P<tag>[0-9]+\.[0-9]+\.[0-9]+)"
26+
+ r"(-rc(?P<rc>[0-9]+))?"
27+
+ r"(-(?P<distance>[0-9]+)-g(?P<hash>[a-f0-9]+))?"
28+
+ r"(\.(?P<dirty>dirty))?"
29+
+ r"$"
30+
)
31+
32+
33+
def convert_sls_version_to_python(sls_version: str) -> str:
34+
match = GIT_REGEX_PATTERN.match(sls_version)
35+
if not match:
36+
raise RuntimeError(f"Invalid SLS version {sls_version}")
2237

23-
if not path.exists(VERSION_PY_PATH):
24-
try:
25-
gitversion = (
26-
subprocess.check_output(
27-
"git describe --tags --always --first-parent".split()
38+
python_version = match.group("tag")
39+
rc_group = match.group("rc")
40+
distance_group = match.group("distance")
41+
hash_group = match.group("hash")
42+
dirty_group = match.group("dirty")
43+
44+
if rc_group:
45+
python_version += "rc" + rc_group
46+
if distance_group:
47+
if not hash_group:
48+
raise RuntimeError(
49+
f"Cannot specify commit distance without hash for version {sls_version}"
2850
)
29-
.decode()
30-
.strip()
31-
.replace("-", "_")
51+
python_version += "+" + distance_group + ".g" + hash_group
52+
if dirty_group:
53+
python_version += "." + dirty_group
54+
return python_version
55+
56+
57+
try:
58+
gitversion = (
59+
subprocess.check_output(
60+
"git describe --tags --always --first-parent".split()
3261
)
33-
open(VERSION_PY_PATH, "w").write(
34-
'__version__ = "{}"\n'.format(gitversion)
62+
.decode()
63+
.strip()
64+
)
65+
open(VERSION_PY_PATH, "w").write(
66+
'__version__ = "{}"\n'.format(
67+
convert_sls_version_to_python(gitversion)
3568
)
36-
if not path.exists("build"):
37-
makedirs("build")
38-
except subprocess.CalledProcessError:
39-
print("outside git repo, not generating new version string")
69+
)
70+
if not path.exists("build"):
71+
makedirs("build")
72+
except subprocess.CalledProcessError:
73+
print("outside git repo, not generating new version string")
4074
exec(open(VERSION_PY_PATH).read())
4175

4276

@@ -83,12 +117,12 @@ def blackCheck(self):
83117
# The project's main homepage.
84118
url="https://github.com/palantir/conjure-python-client",
85119
author="Palantir Technologies, Inc.",
86-
classifiers=[
120+
classifiers=[
87121
"License :: OSI Approved :: Apache Software License",
88122
"Programming Language :: Python :: 3",
89123
"Programming Language :: Python :: 3.8",
90124
"Programming Language :: Python :: 3.9",
91-
"Programming Language :: Python :: 3.10"
125+
"Programming Language :: Python :: 3.10",
92126
],
93127
# You can just specify the packages manually here if your project is
94128
# simple. Or you can use find_packages().

test/http/__init__.py

Whitespace-only changes.

test/http/test_requests_client.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import sys
2+
3+
from conjure_python_client._http.requests_client import (
4+
SOCKET_KEEP_ALIVE,
5+
SOCKET_KEEP_INTVL,
6+
TransportAdapter,
7+
)
8+
9+
if sys.platform != "darwin":
10+
from conjure_python_client._http.requests_client import SOCKET_KEEP_IDLE
11+
12+
13+
def test_can_enable_keep_alives_in_transport_adapter():
14+
assert (
15+
TransportAdapter(
16+
max_retries=12, enable_keep_alive=True
17+
)._enable_keep_alive
18+
is True
19+
)
20+
assert TransportAdapter(max_retries=12)._enable_keep_alive is False
21+
assert TransportAdapter()._enable_keep_alive is False
22+
23+
24+
def test_keep_alive_passes_correct_options():
25+
socket_options = TransportAdapter(
26+
max_retries=12, enable_keep_alive=True
27+
).poolmanager.connection_pool_kw["socket_options"]
28+
assert SOCKET_KEEP_ALIVE in socket_options
29+
assert SOCKET_KEEP_INTVL in socket_options
30+
if sys.platform != "darwin":
31+
assert SOCKET_KEEP_IDLE in socket_options
32+
33+
34+
def test_keep_alive_passed_in_state_in_transport_adapter():
35+
ta = TransportAdapter()
36+
ta.__setstate__(
37+
TransportAdapter(max_retries=12, enable_keep_alive=True).__getstate__()
38+
)
39+
assert ta._enable_keep_alive is True
40+
assert ta.max_retries.total == 12

0 commit comments

Comments
 (0)