mirror of https://github.com/dapr/java-sdk.git
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:
parent
e4cc0303fa
commit
e84d2c4e61
|
@ -145,6 +145,38 @@ public class Properties {
|
||||||
"DAPR_GRPC_ENDPOINT",
|
"DAPR_GRPC_ENDPOINT",
|
||||||
null);
|
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.
|
* GRPC endpoint for remote sidecar connectivity.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -32,9 +32,15 @@ import java.io.InputStream;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.regex.Pattern;
|
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_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_PORT;
|
||||||
import static io.dapr.config.Properties.GRPC_TLS_CA_PATH;
|
import static io.dapr.config.Properties.GRPC_TLS_CA_PATH;
|
||||||
import static io.dapr.config.Properties.GRPC_TLS_CERT_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_HOSTNAME_REGEX_PART = "(([A-Za-z0-9_\\-\\.]+)|(\\[" + IPV6_REGEX + "\\]))";
|
||||||
|
|
||||||
private static final String GRPC_ENDPOINT_DNS_AUTHORITY_REGEX_PART =
|
private static final String GRPC_ENDPOINT_DNS_AUTHORITY_REGEX_PART = "(?<dnsWithAuthority>dns://)"
|
||||||
"(?<dnsWithAuthority>dns://)(?<authorityEndpoint>"
|
+ "(?<authorityEndpoint>"
|
||||||
+ GRPC_ENDPOINT_HOSTNAME_REGEX_PART + ":[0-9]+)?/";
|
+ GRPC_ENDPOINT_HOSTNAME_REGEX_PART + ":[0-9]+)?/";
|
||||||
|
|
||||||
private static final String GRPC_ENDPOINT_PARAM_REGEX_PART = "(\\?(?<param>tls\\=((true)|(false))))?";
|
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) {
|
if (interceptors != null && interceptors.length > 0) {
|
||||||
builder = builder.intercept(interceptors);
|
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();
|
return builder.build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new DaprException(
|
throw new DaprException(
|
||||||
new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR")
|
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);
|
ManagedChannelBuilder<?> builder = ManagedChannelBuilder.forTarget(settings.endpoint);
|
||||||
|
|
||||||
if (clientCertPath != null && clientKeyPath != null) {
|
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 (
|
try (
|
||||||
InputStream clientCertInputStream = new FileInputStream(clientCertPath);
|
InputStream clientCertInputStream = new FileInputStream(clientCertPath);
|
||||||
InputStream clientKeyInputStream = new FileInputStream(clientKeyPath);
|
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()
|
TlsChannelCredentials.Builder builderCreds = TlsChannelCredentials.newBuilder()
|
||||||
.keyManager(clientCertInputStream, clientKeyInputStream); // For client authentication
|
.keyManager(clientCertInputStream, clientKeyInputStream); // For client authentication
|
||||||
if (caCertInputStream != null) {
|
if (caCertInputStream != null) {
|
||||||
builderCreds.trustManager(caCertInputStream); // For server authentication
|
builderCreds.trustManager(caCertInputStream); // For server authentication
|
||||||
}
|
}
|
||||||
ChannelCredentials credentials = builderCreds.build();
|
ChannelCredentials credentials = builderCreds.build();
|
||||||
builder = Grpc.newChannelBuilder(settings.endpoint, credentials);
|
builder = Grpc.newChannelBuilder(settings.endpoint, credentials);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new DaprException(
|
throw new DaprException(
|
||||||
new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR")
|
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) {
|
} else if (caCertPath != null) {
|
||||||
// Simple TLS case - using CA cert only for server authentication
|
// Simple TLS case - using CA cert only for server authentication
|
||||||
|
@ -183,7 +198,8 @@ public final class NetworkUtils {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new DaprException(
|
throw new DaprException(
|
||||||
new DaprError().setErrorCode("TLS_CREDENTIALS_ERROR")
|
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) {
|
} else if (!settings.secure) {
|
||||||
builder = builder.usePlaintext();
|
builder = builder.usePlaintext();
|
||||||
|
@ -194,6 +210,13 @@ public final class NetworkUtils {
|
||||||
if (interceptors != null && interceptors.length > 0) {
|
if (interceptors != null && interceptors.length > 0) {
|
||||||
builder = builder.intercept(interceptors);
|
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();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,13 +228,24 @@ public final class NetworkUtils {
|
||||||
final String tlsCertPath;
|
final String tlsCertPath;
|
||||||
final String tlsCaPath;
|
final String tlsCaPath;
|
||||||
|
|
||||||
|
final boolean enableKeepAlive;
|
||||||
|
final Duration keepAliveTimeSeconds;
|
||||||
|
final Duration keepAliveTimeoutSeconds;
|
||||||
|
final boolean keepAliveWithoutCalls;
|
||||||
|
|
||||||
private GrpcEndpointSettings(
|
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.endpoint = endpoint;
|
||||||
this.secure = secure;
|
this.secure = secure;
|
||||||
this.tlsPrivateKeyPath = tlsPrivateKeyPath;
|
this.tlsPrivateKeyPath = tlsPrivateKeyPath;
|
||||||
this.tlsCertPath = tlsCertPath;
|
this.tlsCertPath = tlsCertPath;
|
||||||
this.tlsCaPath = tlsCaPath;
|
this.tlsCaPath = tlsCaPath;
|
||||||
|
this.enableKeepAlive = enableKeepAlive;
|
||||||
|
this.keepAliveTimeSeconds = keepAliveTimeSeconds;
|
||||||
|
this.keepAliveTimeoutSeconds = keepAliveTimeoutSeconds;
|
||||||
|
this.keepAliveWithoutCalls = keepAliveWithoutCalls;
|
||||||
}
|
}
|
||||||
|
|
||||||
static GrpcEndpointSettings parse(Properties properties) {
|
static GrpcEndpointSettings parse(Properties properties) {
|
||||||
|
@ -220,6 +254,10 @@ public final class NetworkUtils {
|
||||||
String clientKeyPath = properties.getValue(GRPC_TLS_KEY_PATH);
|
String clientKeyPath = properties.getValue(GRPC_TLS_KEY_PATH);
|
||||||
String clientCertPath = properties.getValue(GRPC_TLS_CERT_PATH);
|
String clientCertPath = properties.getValue(GRPC_TLS_CERT_PATH);
|
||||||
String caCertPath = properties.getValue(GRPC_TLS_CA_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;
|
boolean secure = false;
|
||||||
String grpcEndpoint = properties.getValue(GRPC_ENDPOINT);
|
String grpcEndpoint = properties.getValue(GRPC_ENDPOINT);
|
||||||
|
@ -257,30 +295,33 @@ public final class NetworkUtils {
|
||||||
var authorityEndpoint = matcher.group("authorityEndpoint");
|
var authorityEndpoint = matcher.group("authorityEndpoint");
|
||||||
if (authorityEndpoint != null) {
|
if (authorityEndpoint != null) {
|
||||||
return new GrpcEndpointSettings(
|
return new GrpcEndpointSettings(
|
||||||
String.format(
|
String.format(
|
||||||
"dns://%s/%s:%d",
|
"dns://%s/%s:%d",
|
||||||
authorityEndpoint,
|
authorityEndpoint,
|
||||||
address,
|
address,
|
||||||
port
|
port),
|
||||||
), secure, clientKeyPath, clientCertPath, caCertPath);
|
secure, clientKeyPath, clientCertPath, caCertPath, enablekeepAlive, keepAliveTimeSeconds,
|
||||||
|
keepAliveTimeoutSeconds, keepAliveWithoutCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
var socket = matcher.group("socket");
|
var socket = matcher.group("socket");
|
||||||
if (socket != null) {
|
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");
|
var vsocket = matcher.group("vsocket");
|
||||||
if (vsocket != null) {
|
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(
|
return new GrpcEndpointSettings(String.format(
|
||||||
"dns:///%s:%d",
|
"dns:///%s:%d",
|
||||||
address,
|
address,
|
||||||
port
|
port), secure, clientKeyPath, clientCertPath, caCertPath, enablekeepAlive, keepAliveTimeSeconds,
|
||||||
), secure, clientKeyPath, clientCertPath, caCertPath);
|
keepAliveTimeoutSeconds, keepAliveWithoutCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ package io.dapr.utils;
|
||||||
|
|
||||||
import io.dapr.config.Properties;
|
import io.dapr.config.Properties;
|
||||||
import io.dapr.exceptions.DaprException;
|
import io.dapr.exceptions.DaprException;
|
||||||
|
import io.dapr.utils.NetworkUtils.GrpcEndpointSettings;
|
||||||
import io.grpc.ManagedChannel;
|
import io.grpc.ManagedChannel;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
@ -591,4 +592,44 @@ public class NetworkUtilsTest {
|
||||||
// Verify the channel is active and using TLS (not plaintext)
|
// Verify the channel is active and using TLS (not plaintext)
|
||||||
Assertions.assertFalse(channel.isTerminated(), "Channel should be active");
|
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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue