Skip to content

Commit 771bfc6

Browse files
committed
Support http2 keep-alive
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent 93cd8df commit 771bfc6

File tree

2 files changed

+127
-3
lines changed

2 files changed

+127
-3
lines changed

src/frequenz/client/base/channel.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import dataclasses
77
import pathlib
8+
from datetime import timedelta
89
from typing import assert_never
910
from urllib.parse import parse_qs, urlparse
1011

@@ -41,6 +42,24 @@ class SslOptions:
4142
"""
4243

4344

45+
@dataclasses.dataclass(frozen=True)
46+
class KeepAliveOptions:
47+
"""Options for HTTP2 keep-alive pings."""
48+
49+
enabled: bool = True
50+
"""Whether keep-alive should be enabled."""
51+
52+
interval: timedelta | None = None
53+
"""The interval between pings.
54+
55+
If None, keep-alive is disabled."""
56+
57+
timeout: timedelta = timedelta(seconds=20)
58+
"""The time in milliseconds to wait for a keep-alive response.
59+
60+
This is only used if `interval` is not None and `enabled` is `True`."""
61+
62+
4463
@dataclasses.dataclass(frozen=True)
4564
class ChannelOptions:
4665
"""Options for a gRPC channel."""
@@ -51,6 +70,9 @@ class ChannelOptions:
5170
ssl: SslOptions = SslOptions()
5271
"""SSL options for the channel."""
5372

73+
keep_alive: KeepAliveOptions = KeepAliveOptions()
74+
"""HTTP2 keep-alive options for the channel."""
75+
5476

5577
def parse_grpc_uri(
5678
uri: str,
@@ -120,6 +142,23 @@ def parse_grpc_uri(
120142
parsed_uri.netloc if parsed_uri.port else f"{parsed_uri.netloc}:{defaults.port}"
121143
)
122144

145+
channel_options = (
146+
[
147+
("grpc.http2.max_pings_without_data", 0),
148+
("grpc.keepalive_permit_without_calls", 1),
149+
(
150+
"grpc.keepalive_time_ms",
151+
(defaults.keep_alive.interval.total_seconds() * 1000),
152+
),
153+
(
154+
"grpc.keepalive_timeout_ms",
155+
defaults.keep_alive.timeout.total_seconds() * 1000,
156+
),
157+
]
158+
if defaults.keep_alive.enabled and defaults.keep_alive.interval
159+
else None
160+
)
161+
123162
ssl = defaults.ssl.enabled if options.ssl is None else options.ssl
124163
if ssl:
125164
return secure_channel(
@@ -141,8 +180,9 @@ def parse_grpc_uri(
141180
defaults.ssl.certificate_chain,
142181
),
143182
),
183+
channel_options,
144184
)
145-
return insecure_channel(target)
185+
return insecure_channel(target, channel_options)
146186

147187

148188
def _to_bool(value: str) -> bool:

tests/test_channel.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import dataclasses
77
import pathlib
8+
from datetime import timedelta
89
from unittest import mock
910

1011
import pytest
@@ -13,6 +14,7 @@
1314

1415
from frequenz.client.base.channel import (
1516
ChannelOptions,
17+
KeepAliveOptions,
1618
SslOptions,
1719
_to_bool,
1820
parse_grpc_uri,
@@ -136,6 +138,70 @@ class _ValidUrlTestCase:
136138
),
137139
),
138140
),
141+
_ValidUrlTestCase(
142+
title="Keep-alive no defaults",
143+
uri="grpc://localhost:1234",
144+
defaults=ChannelOptions(
145+
keep_alive=KeepAliveOptions(
146+
enabled=True,
147+
interval=timedelta(minutes=5),
148+
timeout=timedelta(minutes=1),
149+
),
150+
),
151+
expected_host="localhost",
152+
expected_port=1234,
153+
expected_options=ChannelOptions(
154+
keep_alive=KeepAliveOptions(
155+
enabled=True,
156+
interval=timedelta(minutes=5),
157+
timeout=timedelta(minutes=1),
158+
),
159+
),
160+
),
161+
_ValidUrlTestCase(
162+
title="Keep-alive default timeout",
163+
uri="grpc://localhost:1234",
164+
defaults=ChannelOptions(
165+
keep_alive=KeepAliveOptions(
166+
enabled=False, interval=timedelta(minutes=5)
167+
),
168+
),
169+
expected_host="localhost",
170+
expected_port=1234,
171+
expected_options=ChannelOptions(
172+
keep_alive=KeepAliveOptions(
173+
enabled=False, interval=timedelta(minutes=5)
174+
),
175+
),
176+
),
177+
_ValidUrlTestCase(
178+
title="Keep-alive default interval",
179+
uri="grpc://localhost:1234",
180+
defaults=ChannelOptions(
181+
keep_alive=KeepAliveOptions(enabled=True, timeout=timedelta(minutes=1)),
182+
),
183+
expected_host="localhost",
184+
expected_port=1234,
185+
expected_options=ChannelOptions(
186+
keep_alive=KeepAliveOptions(enabled=True, timeout=timedelta(minutes=1)),
187+
),
188+
),
189+
_ValidUrlTestCase(
190+
title="keep-alive no defaults",
191+
uri="grpc://localhost:1234",
192+
defaults=ChannelOptions(
193+
keep_alive=KeepAliveOptions(
194+
enabled=False, interval=None, timeout=timedelta(minutes=1)
195+
),
196+
),
197+
expected_host="localhost",
198+
expected_port=1234,
199+
expected_options=ChannelOptions(
200+
keep_alive=KeepAliveOptions(
201+
enabled=False, interval=None, timeout=timedelta(minutes=1)
202+
),
203+
),
204+
),
139205
],
140206
ids=lambda case: case.title,
141207
)
@@ -198,6 +264,22 @@ def test_parse_uri_ok( # pylint: disable=too-many-locals
198264

199265
assert channel == expected_channel
200266
expected_target = f"{expected_host}:{expected_port}"
267+
expected_channel_options = (
268+
[
269+
("grpc.http2.max_pings_without_data", 0),
270+
("grpc.keepalive_permit_without_calls", 1),
271+
(
272+
"grpc.keepalive_time_ms",
273+
(defaults.keep_alive.interval.total_seconds() * 1000),
274+
),
275+
(
276+
"grpc.keepalive_timeout_ms",
277+
defaults.keep_alive.timeout.total_seconds() * 1000,
278+
),
279+
]
280+
if defaults.keep_alive.enabled and defaults.keep_alive.interval
281+
else None
282+
)
201283
if expected_ssl:
202284
if isinstance(expected_root_certificates, pathlib.Path):
203285
get_contents_mock.assert_any_call(
@@ -223,10 +305,12 @@ def test_parse_uri_ok( # pylint: disable=too-many-locals
223305
certificate_chain=expected_certificate_chain,
224306
)
225307
secure_channel_mock.assert_called_once_with(
226-
expected_target, expected_credentials
308+
expected_target, expected_credentials, expected_channel_options
227309
)
228310
else:
229-
insecure_channel_mock.assert_called_once_with(expected_target)
311+
insecure_channel_mock.assert_called_once_with(
312+
expected_target, expected_channel_options
313+
)
230314

231315

232316
@pytest.mark.parametrize("value", ["true", "on", "1", "TrUe", "On", "ON", "TRUE"])

0 commit comments

Comments
 (0)