Skip to content

Commit e84d2c4

Browse files
chore: Add grpc keepalives (#1382)
* chore: Add grpc keepalives Signed-off-by: Javier Aliaga <[email protected]> * chore: Make grpc keepalive configurable Signed-off-by: Javier Aliaga <[email protected]> * chore: Fix review comments Signed-off-by: Javier Aliaga <[email protected]> * chore: Missing keepalive config for GRPC TLS INSECURE Signed-off-by: Javier Aliaga <[email protected]> * chore: Add test Signed-off-by: Javier Aliaga <[email protected]> * fix: Comment typo Signed-off-by: Javier Aliaga <[email protected]> --------- Signed-off-by: Javier Aliaga <[email protected]>
1 parent e4cc030 commit e84d2c4

File tree

4 files changed

+177
-24
lines changed

4 files changed

+177
-24
lines changed

sdk/src/main/java/io/dapr/config/Properties.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,38 @@ public class Properties {
145145
"DAPR_GRPC_ENDPOINT",
146146
null);
147147

148+
/**
149+
* GRPC enable keep alive.
150+
*/
151+
public static final Property<Boolean> GRPC_ENABLE_KEEP_ALIVE = new BooleanProperty(
152+
"dapr.grpc.enableKeepAlive",
153+
"DAPR_GRPC_ENABLE_KEEP_ALIVE",
154+
false);
155+
156+
/**
157+
* GRPC keep alive time in seconds.
158+
*/
159+
public static final Property<Duration> GRPC_KEEP_ALIVE_TIME_SECONDS = new SecondsDurationProperty(
160+
"dapr.grpc.keepAliveTimeSeconds",
161+
"DAPR_GRPC_KEEP_ALIVE_TIME_SECONDS",
162+
Duration.ofSeconds(10));
163+
164+
/**
165+
* GRPC keep alive timeout in seconds.
166+
*/
167+
public static final Property<Duration> GRPC_KEEP_ALIVE_TIMEOUT_SECONDS = new SecondsDurationProperty(
168+
"dapr.grpc.keepAliveTimeoutSeconds",
169+
"DAPR_GRPC_KEEP_ALIVE_TIMEOUT_SECONDS",
170+
Duration.ofSeconds(5));
171+
172+
/**
173+
* GRPC keep alive without calls.
174+
*/
175+
public static final Property<Boolean> GRPC_KEEP_ALIVE_WITHOUT_CALLS = new BooleanProperty(
176+
"dapr.grpc.keepAliveWithoutCalls",
177+
"DAPR_GRPC_KEEP_ALIVE_WITHOUT_CALLS",
178+
true);
179+
148180
/**
149181
* GRPC endpoint for remote sidecar connectivity.
150182
*/
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.config;
15+
16+
import java.time.Duration;
17+
18+
/**
19+
* Integer configuration property.
20+
*/
21+
public class SecondsDurationProperty extends Property<Duration> {
22+
23+
/**
24+
* {@inheritDoc}
25+
*/
26+
SecondsDurationProperty(String name, String envName, Duration defaultValue) {
27+
super(name, envName, defaultValue);
28+
}
29+
30+
/**
31+
* {@inheritDoc}
32+
*/
33+
@Override
34+
protected Duration parse(String value) {
35+
long longValue = Long.parseLong(value);
36+
return Duration.ofSeconds(longValue);
37+
}
38+
39+
}

sdk/src/main/java/io/dapr/utils/NetworkUtils.java

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,15 @@
3232
import java.net.InetAddress;
3333
import java.net.InetSocketAddress;
3434
import java.net.Socket;
35+
import java.time.Duration;
36+
import java.util.concurrent.TimeUnit;
3537
import java.util.regex.Pattern;
3638

39+
import static io.dapr.config.Properties.GRPC_ENABLE_KEEP_ALIVE;
3740
import static io.dapr.config.Properties.GRPC_ENDPOINT;
41+
import static io.dapr.config.Properties.GRPC_KEEP_ALIVE_TIMEOUT_SECONDS;
42+
import static io.dapr.config.Properties.GRPC_KEEP_ALIVE_TIME_SECONDS;
43+
import static io.dapr.config.Properties.GRPC_KEEP_ALIVE_WITHOUT_CALLS;
3844
import static io.dapr.config.Properties.GRPC_PORT;
3945
import static io.dapr.config.Properties.GRPC_TLS_CA_PATH;
4046
import static io.dapr.config.Properties.GRPC_TLS_CERT_PATH;
@@ -68,8 +74,8 @@ public final class NetworkUtils {
6874

6975
private static final String GRPC_ENDPOINT_HOSTNAME_REGEX_PART = "(([A-Za-z0-9_\\-\\.]+)|(\\[" + IPV6_REGEX + "\\]))";
7076

71-
private static final String GRPC_ENDPOINT_DNS_AUTHORITY_REGEX_PART =
72-
"(?<dnsWithAuthority>dns://)(?<authorityEndpoint>"
77+
private static final String GRPC_ENDPOINT_DNS_AUTHORITY_REGEX_PART = "(?<dnsWithAuthority>dns://)"
78+
+ "(?<authorityEndpoint>"
7379
+ GRPC_ENDPOINT_HOSTNAME_REGEX_PART + ":[0-9]+)?/";
7480

7581
private static final String GRPC_ENDPOINT_PARAM_REGEX_PART = "(\\?(?<param>tls\\=((true)|(false))))?";
@@ -140,11 +146,19 @@ public static ManagedChannel buildGrpcManagedChannel(Properties properties, Clie
140146
if (interceptors != null && interceptors.length > 0) {
141147
builder = builder.intercept(interceptors);
142148
}
149+
150+
if (settings.enableKeepAlive) {
151+
builder.keepAliveTime(settings.keepAliveTimeSeconds.toSeconds(), TimeUnit.SECONDS)
152+
.keepAliveTimeout(settings.keepAliveTimeoutSeconds.toSeconds(), TimeUnit.SECONDS)
153+
.keepAliveWithoutCalls(settings.keepAliveWithoutCalls);
154+
}
155+
143156
return builder.build();
144157
} catch (Exception e) {
145158
throw new DaprException(
146159
new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR")
147-
.setMessage("Failed to create insecure TLS credentials"), e);
160+
.setMessage("Failed to create insecure TLS credentials"),
161+
e);
148162
}
149163
}
150164

@@ -155,23 +169,24 @@ public static ManagedChannel buildGrpcManagedChannel(Properties properties, Clie
155169
ManagedChannelBuilder<?> builder = ManagedChannelBuilder.forTarget(settings.endpoint);
156170

157171
if (clientCertPath != null && clientKeyPath != null) {
158-
// mTLS case - using client cert and key, with optional CA cert for server authentication
172+
// mTLS case - using client cert and key, with optional CA cert for server
173+
// authentication
159174
try (
160175
InputStream clientCertInputStream = new FileInputStream(clientCertPath);
161176
InputStream clientKeyInputStream = new FileInputStream(clientKeyPath);
162-
InputStream caCertInputStream = caCertPath != null ? new FileInputStream(caCertPath) : null
163-
) {
177+
InputStream caCertInputStream = caCertPath != null ? new FileInputStream(caCertPath) : null) {
164178
TlsChannelCredentials.Builder builderCreds = TlsChannelCredentials.newBuilder()
165-
.keyManager(clientCertInputStream, clientKeyInputStream); // For client authentication
179+
.keyManager(clientCertInputStream, clientKeyInputStream); // For client authentication
166180
if (caCertInputStream != null) {
167-
builderCreds.trustManager(caCertInputStream); // For server authentication
181+
builderCreds.trustManager(caCertInputStream); // For server authentication
168182
}
169183
ChannelCredentials credentials = builderCreds.build();
170184
builder = Grpc.newChannelBuilder(settings.endpoint, credentials);
171185
} catch (IOException e) {
172186
throw new DaprException(
173187
new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR")
174-
.setMessage("Failed to create mTLS credentials" + (caCertPath != null ? " with CA cert" : "")), e);
188+
.setMessage("Failed to create mTLS credentials" + (caCertPath != null ? " with CA cert" : "")),
189+
e);
175190
}
176191
} else if (caCertPath != null) {
177192
// Simple TLS case - using CA cert only for server authentication
@@ -183,7 +198,8 @@ public static ManagedChannel buildGrpcManagedChannel(Properties properties, Clie
183198
} catch (IOException e) {
184199
throw new DaprException(
185200
new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR")
186-
.setMessage("Failed to create TLS credentials with CA cert"), e);
201+
.setMessage("Failed to create TLS credentials with CA cert"),
202+
e);
187203
}
188204
} else if (!settings.secure) {
189205
builder = builder.usePlaintext();
@@ -194,6 +210,13 @@ public static ManagedChannel buildGrpcManagedChannel(Properties properties, Clie
194210
if (interceptors != null && interceptors.length > 0) {
195211
builder = builder.intercept(interceptors);
196212
}
213+
214+
if (settings.enableKeepAlive) {
215+
builder.keepAliveTime(settings.keepAliveTimeSeconds.toSeconds(), TimeUnit.SECONDS)
216+
.keepAliveTimeout(settings.keepAliveTimeoutSeconds.toSeconds(), TimeUnit.SECONDS)
217+
.keepAliveWithoutCalls(settings.keepAliveWithoutCalls);
218+
}
219+
197220
return builder.build();
198221
}
199222

@@ -205,13 +228,24 @@ static final class GrpcEndpointSettings {
205228
final String tlsCertPath;
206229
final String tlsCaPath;
207230

231+
final boolean enableKeepAlive;
232+
final Duration keepAliveTimeSeconds;
233+
final Duration keepAliveTimeoutSeconds;
234+
final boolean keepAliveWithoutCalls;
235+
208236
private GrpcEndpointSettings(
209-
String endpoint, boolean secure, String tlsPrivateKeyPath, String tlsCertPath, String tlsCaPath) {
237+
String endpoint, boolean secure, String tlsPrivateKeyPath, String tlsCertPath, String tlsCaPath,
238+
boolean enableKeepAlive, Duration keepAliveTimeSeconds, Duration keepAliveTimeoutSeconds,
239+
boolean keepAliveWithoutCalls) {
210240
this.endpoint = endpoint;
211241
this.secure = secure;
212242
this.tlsPrivateKeyPath = tlsPrivateKeyPath;
213243
this.tlsCertPath = tlsCertPath;
214244
this.tlsCaPath = tlsCaPath;
245+
this.enableKeepAlive = enableKeepAlive;
246+
this.keepAliveTimeSeconds = keepAliveTimeSeconds;
247+
this.keepAliveTimeoutSeconds = keepAliveTimeoutSeconds;
248+
this.keepAliveWithoutCalls = keepAliveWithoutCalls;
215249
}
216250

217251
static GrpcEndpointSettings parse(Properties properties) {
@@ -220,6 +254,10 @@ static GrpcEndpointSettings parse(Properties properties) {
220254
String clientKeyPath = properties.getValue(GRPC_TLS_KEY_PATH);
221255
String clientCertPath = properties.getValue(GRPC_TLS_CERT_PATH);
222256
String caCertPath = properties.getValue(GRPC_TLS_CA_PATH);
257+
boolean enablekeepAlive = properties.getValue(GRPC_ENABLE_KEEP_ALIVE);
258+
Duration keepAliveTimeSeconds = properties.getValue(GRPC_KEEP_ALIVE_TIME_SECONDS);
259+
Duration keepAliveTimeoutSeconds = properties.getValue(GRPC_KEEP_ALIVE_TIMEOUT_SECONDS);
260+
boolean keepAliveWithoutCalls = properties.getValue(GRPC_KEEP_ALIVE_WITHOUT_CALLS);
223261

224262
boolean secure = false;
225263
String grpcEndpoint = properties.getValue(GRPC_ENDPOINT);
@@ -257,30 +295,33 @@ static GrpcEndpointSettings parse(Properties properties) {
257295
var authorityEndpoint = matcher.group("authorityEndpoint");
258296
if (authorityEndpoint != null) {
259297
return new GrpcEndpointSettings(
260-
String.format(
261-
"dns://%s/%s:%d",
262-
authorityEndpoint,
263-
address,
264-
port
265-
), secure, clientKeyPath, clientCertPath, caCertPath);
298+
String.format(
299+
"dns://%s/%s:%d",
300+
authorityEndpoint,
301+
address,
302+
port),
303+
secure, clientKeyPath, clientCertPath, caCertPath, enablekeepAlive, keepAliveTimeSeconds,
304+
keepAliveTimeoutSeconds, keepAliveWithoutCalls);
266305
}
267306

268307
var socket = matcher.group("socket");
269308
if (socket != null) {
270-
return new GrpcEndpointSettings(socket, secure, clientKeyPath, clientCertPath, caCertPath);
309+
return new GrpcEndpointSettings(socket, secure, clientKeyPath, clientCertPath, caCertPath, enablekeepAlive,
310+
keepAliveTimeSeconds, keepAliveTimeoutSeconds, keepAliveWithoutCalls);
271311
}
272312

273313
var vsocket = matcher.group("vsocket");
274314
if (vsocket != null) {
275-
return new GrpcEndpointSettings(vsocket, secure, clientKeyPath, clientCertPath, caCertPath);
315+
return new GrpcEndpointSettings(vsocket, secure, clientKeyPath, clientCertPath, caCertPath, enablekeepAlive,
316+
keepAliveTimeSeconds, keepAliveTimeoutSeconds, keepAliveWithoutCalls);
276317
}
277318
}
278319

279320
return new GrpcEndpointSettings(String.format(
280-
"dns:///%s:%d",
281-
address,
282-
port
283-
), secure, clientKeyPath, clientCertPath, caCertPath);
321+
"dns:///%s:%d",
322+
address,
323+
port), secure, clientKeyPath, clientCertPath, caCertPath, enablekeepAlive, keepAliveTimeSeconds,
324+
keepAliveTimeoutSeconds, keepAliveWithoutCalls);
284325
}
285326

286327
}

sdk/src/test/java/io/dapr/utils/NetworkUtilsTest.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import io.dapr.config.Properties;
1717
import io.dapr.exceptions.DaprException;
18+
import io.dapr.utils.NetworkUtils.GrpcEndpointSettings;
1819
import io.grpc.ManagedChannel;
1920
import org.junit.Assert;
2021
import org.junit.jupiter.api.AfterAll;
@@ -591,4 +592,44 @@ public void testBuildGrpcManagedChannelWithInsecureTlsAndCustomEndpoint() throws
591592
// Verify the channel is active and using TLS (not plaintext)
592593
Assertions.assertFalse(channel.isTerminated(), "Channel should be active");
593594
}
594-
}
595+
596+
@Test
597+
public void testBuildGrpcManagedChannelWithKeepAliveDefaults() throws Exception {
598+
var properties = new Properties(Map.of(
599+
Properties.GRPC_ENABLE_KEEP_ALIVE.getName(), "true"
600+
));
601+
602+
channel = NetworkUtils.buildGrpcManagedChannel(properties);
603+
channels.add(channel);
604+
605+
// Verify the channel is active and using TLS (not plaintext)
606+
Assertions.assertFalse(channel.isTerminated(), "Channel should be active");
607+
}
608+
609+
@Test
610+
public void testDefaultKeepAliveSettings() throws Exception {
611+
Properties properties = new Properties();
612+
613+
GrpcEndpointSettings settings = NetworkUtils.GrpcEndpointSettings.parse(properties);
614+
Assertions.assertEquals(false, settings.enableKeepAlive);
615+
Assertions.assertEquals(10, settings.keepAliveTimeSeconds.getSeconds());
616+
Assertions.assertEquals(5, settings.keepAliveTimeoutSeconds.getSeconds());
617+
Assertions.assertEquals(true, settings.keepAliveWithoutCalls);
618+
}
619+
620+
@Test
621+
public void testDefaultKeepAliveOverride() throws Exception {
622+
Properties properties = new Properties(Map.of(
623+
Properties.GRPC_ENABLE_KEEP_ALIVE.getName(), "true",
624+
Properties.GRPC_KEEP_ALIVE_TIME_SECONDS.getName(), "100",
625+
Properties.GRPC_KEEP_ALIVE_TIMEOUT_SECONDS.getName(), "50",
626+
Properties.GRPC_KEEP_ALIVE_WITHOUT_CALLS.getName(), "false"
627+
));
628+
629+
GrpcEndpointSettings settings = NetworkUtils.GrpcEndpointSettings.parse(properties);
630+
Assertions.assertEquals(true, settings.enableKeepAlive);
631+
Assertions.assertEquals(100, settings.keepAliveTimeSeconds.getSeconds());
632+
Assertions.assertEquals(50, settings.keepAliveTimeoutSeconds.getSeconds());
633+
Assertions.assertEquals(false, settings.keepAliveWithoutCalls);
634+
}
635+
}

0 commit comments

Comments
 (0)