chore: Add grpc keepalives (#1382)

* chore: Add grpc keepalives

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: Make grpc keepalive configurable

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: Fix review comments

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: Missing keepalive config for GRPC TLS INSECURE

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: Add test

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* fix: Comment typo

Signed-off-by: Javier Aliaga <javier@diagrid.io>

---------

Signed-off-by: Javier Aliaga <javier@diagrid.io>
This commit is contained in:
Javier Aliaga 2025-05-21 16:53:15 +02:00 committed by GitHub
parent e4cc0303fa
commit e84d2c4e61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 177 additions and 24 deletions

View File

@ -145,6 +145,38 @@ public class Properties {
"DAPR_GRPC_ENDPOINT",
null);
/**
* GRPC enable keep alive.
*/
public static final Property<Boolean> GRPC_ENABLE_KEEP_ALIVE = new BooleanProperty(
"dapr.grpc.enableKeepAlive",
"DAPR_GRPC_ENABLE_KEEP_ALIVE",
false);
/**
* GRPC keep alive time in seconds.
*/
public static final Property<Duration> GRPC_KEEP_ALIVE_TIME_SECONDS = new SecondsDurationProperty(
"dapr.grpc.keepAliveTimeSeconds",
"DAPR_GRPC_KEEP_ALIVE_TIME_SECONDS",
Duration.ofSeconds(10));
/**
* GRPC keep alive timeout in seconds.
*/
public static final Property<Duration> GRPC_KEEP_ALIVE_TIMEOUT_SECONDS = new SecondsDurationProperty(
"dapr.grpc.keepAliveTimeoutSeconds",
"DAPR_GRPC_KEEP_ALIVE_TIMEOUT_SECONDS",
Duration.ofSeconds(5));
/**
* GRPC keep alive without calls.
*/
public static final Property<Boolean> GRPC_KEEP_ALIVE_WITHOUT_CALLS = new BooleanProperty(
"dapr.grpc.keepAliveWithoutCalls",
"DAPR_GRPC_KEEP_ALIVE_WITHOUT_CALLS",
true);
/**
* GRPC endpoint for remote sidecar connectivity.
*/

View File

@ -0,0 +1,39 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.config;
import java.time.Duration;
/**
* Integer configuration property.
*/
public class SecondsDurationProperty extends Property<Duration> {
/**
* {@inheritDoc}
*/
SecondsDurationProperty(String name, String envName, Duration defaultValue) {
super(name, envName, defaultValue);
}
/**
* {@inheritDoc}
*/
@Override
protected Duration parse(String value) {
long longValue = Long.parseLong(value);
return Duration.ofSeconds(longValue);
}
}

View File

@ -32,9 +32,15 @@ import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import static io.dapr.config.Properties.GRPC_ENABLE_KEEP_ALIVE;
import static io.dapr.config.Properties.GRPC_ENDPOINT;
import static io.dapr.config.Properties.GRPC_KEEP_ALIVE_TIMEOUT_SECONDS;
import static io.dapr.config.Properties.GRPC_KEEP_ALIVE_TIME_SECONDS;
import static io.dapr.config.Properties.GRPC_KEEP_ALIVE_WITHOUT_CALLS;
import static io.dapr.config.Properties.GRPC_PORT;
import static io.dapr.config.Properties.GRPC_TLS_CA_PATH;
import static io.dapr.config.Properties.GRPC_TLS_CERT_PATH;
@ -68,8 +74,8 @@ public final class NetworkUtils {
private static final String GRPC_ENDPOINT_HOSTNAME_REGEX_PART = "(([A-Za-z0-9_\\-\\.]+)|(\\[" + IPV6_REGEX + "\\]))";
private static final String GRPC_ENDPOINT_DNS_AUTHORITY_REGEX_PART =
"(?<dnsWithAuthority>dns://)(?<authorityEndpoint>"
private static final String GRPC_ENDPOINT_DNS_AUTHORITY_REGEX_PART = "(?<dnsWithAuthority>dns://)"
+ "(?<authorityEndpoint>"
+ GRPC_ENDPOINT_HOSTNAME_REGEX_PART + ":[0-9]+)?/";
private static final String GRPC_ENDPOINT_PARAM_REGEX_PART = "(\\?(?<param>tls\\=((true)|(false))))?";
@ -140,11 +146,19 @@ public final class NetworkUtils {
if (interceptors != null && interceptors.length > 0) {
builder = builder.intercept(interceptors);
}
if (settings.enableKeepAlive) {
builder.keepAliveTime(settings.keepAliveTimeSeconds.toSeconds(), TimeUnit.SECONDS)
.keepAliveTimeout(settings.keepAliveTimeoutSeconds.toSeconds(), TimeUnit.SECONDS)
.keepAliveWithoutCalls(settings.keepAliveWithoutCalls);
}
return builder.build();
} catch (Exception e) {
throw new DaprException(
new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR")
.setMessage("Failed to create insecure TLS credentials"), e);
.setMessage("Failed to create insecure TLS credentials"),
e);
}
}
@ -155,23 +169,24 @@ public final class NetworkUtils {
ManagedChannelBuilder<?> builder = ManagedChannelBuilder.forTarget(settings.endpoint);
if (clientCertPath != null && clientKeyPath != null) {
// mTLS case - using client cert and key, with optional CA cert for server authentication
// mTLS case - using client cert and key, with optional CA cert for server
// authentication
try (
InputStream clientCertInputStream = new FileInputStream(clientCertPath);
InputStream clientKeyInputStream = new FileInputStream(clientKeyPath);
InputStream caCertInputStream = caCertPath != null ? new FileInputStream(caCertPath) : null
) {
InputStream caCertInputStream = caCertPath != null ? new FileInputStream(caCertPath) : null) {
TlsChannelCredentials.Builder builderCreds = TlsChannelCredentials.newBuilder()
.keyManager(clientCertInputStream, clientKeyInputStream); // For client authentication
.keyManager(clientCertInputStream, clientKeyInputStream); // For client authentication
if (caCertInputStream != null) {
builderCreds.trustManager(caCertInputStream); // For server authentication
builderCreds.trustManager(caCertInputStream); // For server authentication
}
ChannelCredentials credentials = builderCreds.build();
builder = Grpc.newChannelBuilder(settings.endpoint, credentials);
} catch (IOException e) {
throw new DaprException(
new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR")
.setMessage("Failed to create mTLS credentials" + (caCertPath != null ? " with CA cert" : "")), e);
.setMessage("Failed to create mTLS credentials" + (caCertPath != null ? " with CA cert" : "")),
e);
}
} else if (caCertPath != null) {
// Simple TLS case - using CA cert only for server authentication
@ -183,7 +198,8 @@ public final class NetworkUtils {
} catch (IOException e) {
throw new DaprException(
new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR")
.setMessage("Failed to create TLS credentials with CA cert"), e);
.setMessage("Failed to create TLS credentials with CA cert"),
e);
}
} else if (!settings.secure) {
builder = builder.usePlaintext();
@ -194,6 +210,13 @@ public final class NetworkUtils {
if (interceptors != null && interceptors.length > 0) {
builder = builder.intercept(interceptors);
}
if (settings.enableKeepAlive) {
builder.keepAliveTime(settings.keepAliveTimeSeconds.toSeconds(), TimeUnit.SECONDS)
.keepAliveTimeout(settings.keepAliveTimeoutSeconds.toSeconds(), TimeUnit.SECONDS)
.keepAliveWithoutCalls(settings.keepAliveWithoutCalls);
}
return builder.build();
}
@ -205,13 +228,24 @@ public final class NetworkUtils {
final String tlsCertPath;
final String tlsCaPath;
final boolean enableKeepAlive;
final Duration keepAliveTimeSeconds;
final Duration keepAliveTimeoutSeconds;
final boolean keepAliveWithoutCalls;
private GrpcEndpointSettings(
String endpoint, boolean secure, String tlsPrivateKeyPath, String tlsCertPath, String tlsCaPath) {
String endpoint, boolean secure, String tlsPrivateKeyPath, String tlsCertPath, String tlsCaPath,
boolean enableKeepAlive, Duration keepAliveTimeSeconds, Duration keepAliveTimeoutSeconds,
boolean keepAliveWithoutCalls) {
this.endpoint = endpoint;
this.secure = secure;
this.tlsPrivateKeyPath = tlsPrivateKeyPath;
this.tlsCertPath = tlsCertPath;
this.tlsCaPath = tlsCaPath;
this.enableKeepAlive = enableKeepAlive;
this.keepAliveTimeSeconds = keepAliveTimeSeconds;
this.keepAliveTimeoutSeconds = keepAliveTimeoutSeconds;
this.keepAliveWithoutCalls = keepAliveWithoutCalls;
}
static GrpcEndpointSettings parse(Properties properties) {
@ -220,6 +254,10 @@ public final class NetworkUtils {
String clientKeyPath = properties.getValue(GRPC_TLS_KEY_PATH);
String clientCertPath = properties.getValue(GRPC_TLS_CERT_PATH);
String caCertPath = properties.getValue(GRPC_TLS_CA_PATH);
boolean enablekeepAlive = properties.getValue(GRPC_ENABLE_KEEP_ALIVE);
Duration keepAliveTimeSeconds = properties.getValue(GRPC_KEEP_ALIVE_TIME_SECONDS);
Duration keepAliveTimeoutSeconds = properties.getValue(GRPC_KEEP_ALIVE_TIMEOUT_SECONDS);
boolean keepAliveWithoutCalls = properties.getValue(GRPC_KEEP_ALIVE_WITHOUT_CALLS);
boolean secure = false;
String grpcEndpoint = properties.getValue(GRPC_ENDPOINT);
@ -257,30 +295,33 @@ public final class NetworkUtils {
var authorityEndpoint = matcher.group("authorityEndpoint");
if (authorityEndpoint != null) {
return new GrpcEndpointSettings(
String.format(
"dns://%s/%s:%d",
authorityEndpoint,
address,
port
), secure, clientKeyPath, clientCertPath, caCertPath);
String.format(
"dns://%s/%s:%d",
authorityEndpoint,
address,
port),
secure, clientKeyPath, clientCertPath, caCertPath, enablekeepAlive, keepAliveTimeSeconds,
keepAliveTimeoutSeconds, keepAliveWithoutCalls);
}
var socket = matcher.group("socket");
if (socket != null) {
return new GrpcEndpointSettings(socket, secure, clientKeyPath, clientCertPath, caCertPath);
return new GrpcEndpointSettings(socket, secure, clientKeyPath, clientCertPath, caCertPath, enablekeepAlive,
keepAliveTimeSeconds, keepAliveTimeoutSeconds, keepAliveWithoutCalls);
}
var vsocket = matcher.group("vsocket");
if (vsocket != null) {
return new GrpcEndpointSettings(vsocket, secure, clientKeyPath, clientCertPath, caCertPath);
return new GrpcEndpointSettings(vsocket, secure, clientKeyPath, clientCertPath, caCertPath, enablekeepAlive,
keepAliveTimeSeconds, keepAliveTimeoutSeconds, keepAliveWithoutCalls);
}
}
return new GrpcEndpointSettings(String.format(
"dns:///%s:%d",
address,
port
), secure, clientKeyPath, clientCertPath, caCertPath);
"dns:///%s:%d",
address,
port), secure, clientKeyPath, clientCertPath, caCertPath, enablekeepAlive, keepAliveTimeSeconds,
keepAliveTimeoutSeconds, keepAliveWithoutCalls);
}
}

View File

@ -15,6 +15,7 @@ package io.dapr.utils;
import io.dapr.config.Properties;
import io.dapr.exceptions.DaprException;
import io.dapr.utils.NetworkUtils.GrpcEndpointSettings;
import io.grpc.ManagedChannel;
import org.junit.Assert;
import org.junit.jupiter.api.AfterAll;
@ -591,4 +592,44 @@ public class NetworkUtilsTest {
// Verify the channel is active and using TLS (not plaintext)
Assertions.assertFalse(channel.isTerminated(), "Channel should be active");
}
}
@Test
public void testBuildGrpcManagedChannelWithKeepAliveDefaults() throws Exception {
var properties = new Properties(Map.of(
Properties.GRPC_ENABLE_KEEP_ALIVE.getName(), "true"
));
channel = NetworkUtils.buildGrpcManagedChannel(properties);
channels.add(channel);
// Verify the channel is active and using TLS (not plaintext)
Assertions.assertFalse(channel.isTerminated(), "Channel should be active");
}
@Test
public void testDefaultKeepAliveSettings() throws Exception {
Properties properties = new Properties();
GrpcEndpointSettings settings = NetworkUtils.GrpcEndpointSettings.parse(properties);
Assertions.assertEquals(false, settings.enableKeepAlive);
Assertions.assertEquals(10, settings.keepAliveTimeSeconds.getSeconds());
Assertions.assertEquals(5, settings.keepAliveTimeoutSeconds.getSeconds());
Assertions.assertEquals(true, settings.keepAliveWithoutCalls);
}
@Test
public void testDefaultKeepAliveOverride() throws Exception {
Properties properties = new Properties(Map.of(
Properties.GRPC_ENABLE_KEEP_ALIVE.getName(), "true",
Properties.GRPC_KEEP_ALIVE_TIME_SECONDS.getName(), "100",
Properties.GRPC_KEEP_ALIVE_TIMEOUT_SECONDS.getName(), "50",
Properties.GRPC_KEEP_ALIVE_WITHOUT_CALLS.getName(), "false"
));
GrpcEndpointSettings settings = NetworkUtils.GrpcEndpointSettings.parse(properties);
Assertions.assertEquals(true, settings.enableKeepAlive);
Assertions.assertEquals(100, settings.keepAliveTimeSeconds.getSeconds());
Assertions.assertEquals(50, settings.keepAliveTimeoutSeconds.getSeconds());
Assertions.assertEquals(false, settings.keepAliveWithoutCalls);
}
}