Skip to content

Commit a116c55

Browse files
authored
feat: Add optional keep_alive (#2842)
1 parent 856e5bc commit a116c55

File tree

3 files changed

+95
-3
lines changed

3 files changed

+95
-3
lines changed

sentry_sdk/consts.py

+1
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ def __init__(
264264
ignore_errors=[], # type: Sequence[Union[type, str]] # noqa: B006
265265
max_request_body_size="medium", # type: str
266266
socket_options=None, # type: Optional[List[Tuple[int, int, int | bytes]]]
267+
keep_alive=False, # type: bool
267268
before_send=None, # type: Optional[EventProcessor]
268269
before_breadcrumb=None, # type: Optional[BreadcrumbProcessor]
269270
debug=None, # type: Optional[bool]

sentry_sdk/transport.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io
44
import gzip
5+
import socket
56
import time
67
from datetime import timedelta
78
from collections import defaultdict
@@ -21,6 +22,7 @@
2122
from typing import Callable
2223
from typing import Dict
2324
from typing import Iterable
25+
from typing import List
2426
from typing import Optional
2527
from typing import Tuple
2628
from typing import Type
@@ -40,6 +42,21 @@
4042
from urllib import getproxies # type: ignore
4143

4244

45+
KEEP_ALIVE_SOCKET_OPTIONS = []
46+
for option in [
47+
(socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009
48+
(socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009
49+
(socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009
50+
(socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009
51+
]:
52+
try:
53+
KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2]))
54+
except AttributeError:
55+
# a specific option might not be available on specific systems,
56+
# e.g. TCP_KEEPIDLE doesn't exist on macOS
57+
pass
58+
59+
4360
class Transport(object):
4461
"""Baseclass for all transports.
4562
@@ -446,8 +463,22 @@ def _get_pool_options(self, ca_certs):
446463
"ca_certs": ca_certs or certifi.where(),
447464
}
448465

449-
if self.options["socket_options"]:
450-
options["socket_options"] = self.options["socket_options"]
466+
socket_options = None # type: Optional[List[Tuple[int, int, int | bytes]]]
467+
468+
if self.options["socket_options"] is not None:
469+
socket_options = self.options["socket_options"]
470+
471+
if self.options["keep_alive"]:
472+
if socket_options is None:
473+
socket_options = []
474+
475+
used_options = {(o[0], o[1]) for o in socket_options}
476+
for default_option in KEEP_ALIVE_SOCKET_OPTIONS:
477+
if (default_option[0], default_option[1]) not in used_options:
478+
socket_options.append(default_option)
479+
480+
if socket_options is not None:
481+
options["socket_options"] = socket_options
451482

452483
return options
453484

tests/test_transport.py

+61-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from sentry_sdk import Hub, Client, add_breadcrumb, capture_message, Scope
1515
from sentry_sdk._compat import datetime_utcnow
16-
from sentry_sdk.transport import _parse_rate_limits
16+
from sentry_sdk.transport import KEEP_ALIVE_SOCKET_OPTIONS, _parse_rate_limits
1717
from sentry_sdk.envelope import Envelope, parse_json
1818
from sentry_sdk.integrations.logging import LoggingIntegration
1919

@@ -167,6 +167,66 @@ def test_socket_options(make_client):
167167
assert options["socket_options"] == socket_options
168168

169169

170+
def test_keep_alive_true(make_client):
171+
client = make_client(keep_alive=True)
172+
173+
options = client.transport._get_pool_options([])
174+
assert options["socket_options"] == KEEP_ALIVE_SOCKET_OPTIONS
175+
176+
177+
def test_keep_alive_off_by_default(make_client):
178+
client = make_client()
179+
options = client.transport._get_pool_options([])
180+
assert "socket_options" not in options
181+
182+
183+
def test_socket_options_override_keep_alive(make_client):
184+
socket_options = [
185+
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
186+
(socket.SOL_TCP, socket.TCP_KEEPINTVL, 10),
187+
(socket.SOL_TCP, socket.TCP_KEEPCNT, 6),
188+
]
189+
190+
client = make_client(socket_options=socket_options, keep_alive=False)
191+
192+
options = client.transport._get_pool_options([])
193+
assert options["socket_options"] == socket_options
194+
195+
196+
def test_socket_options_merge_with_keep_alive(make_client):
197+
socket_options = [
198+
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 42),
199+
(socket.SOL_TCP, socket.TCP_KEEPINTVL, 42),
200+
]
201+
202+
client = make_client(socket_options=socket_options, keep_alive=True)
203+
204+
options = client.transport._get_pool_options([])
205+
try:
206+
assert options["socket_options"] == [
207+
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 42),
208+
(socket.SOL_TCP, socket.TCP_KEEPINTVL, 42),
209+
(socket.SOL_TCP, socket.TCP_KEEPIDLE, 45),
210+
(socket.SOL_TCP, socket.TCP_KEEPCNT, 6),
211+
]
212+
except AttributeError:
213+
assert options["socket_options"] == [
214+
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 42),
215+
(socket.SOL_TCP, socket.TCP_KEEPINTVL, 42),
216+
(socket.SOL_TCP, socket.TCP_KEEPCNT, 6),
217+
]
218+
219+
220+
def test_socket_options_override_defaults(make_client):
221+
# If socket_options are set to [], this doesn't mean the user doesn't want
222+
# any custom socket_options, but rather that they want to disable the urllib3
223+
# socket option defaults, so we need to set this and not ignore it.
224+
client = make_client(socket_options=[])
225+
226+
options = client.transport._get_pool_options([])
227+
assert options["socket_options"] == []
228+
229+
170230
def test_transport_infinite_loop(capturing_server, request, make_client):
171231
client = make_client(
172232
debug=True,

0 commit comments

Comments
 (0)