Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions sdk/src/main/java/io/dapr/config/Properties.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
39 changes: 39 additions & 0 deletions sdk/src/main/java/io/dapr/config/SecondsDurationProperty.java
Original file line number Diff line number Diff line change
@@ -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);
}

}
87 changes: 64 additions & 23 deletions sdk/src/main/java/io/dapr/utils/NetworkUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@
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;
Expand Down Expand Up @@ -68,8 +74,8 @@

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))))?";
Expand Down Expand Up @@ -140,11 +146,19 @@
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);

Check warning on line 153 in sdk/src/main/java/io/dapr/utils/NetworkUtils.java

View check run for this annotation

Codecov / codecov/patch

sdk/src/main/java/io/dapr/utils/NetworkUtils.java#L151-L153

Added lines #L151 - L153 were not covered by tests
}

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"),

Check warning on line 160 in sdk/src/main/java/io/dapr/utils/NetworkUtils.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L160 was not covered by tests
e);
}
}

Expand All @@ -155,23 +169,24 @@
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
Expand All @@ -183,7 +198,8 @@
} 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();
Expand All @@ -194,6 +210,13 @@
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();
}

Expand All @@ -205,13 +228,24 @@
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) {
Expand All @@ -220,6 +254,10 @@
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);
Expand Down Expand Up @@ -257,30 +295,33 @@
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);
}

}
Expand Down
43 changes: 42 additions & 1 deletion sdk/src/test/java/io/dapr/utils/NetworkUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

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;
Expand Down Expand Up @@ -591,4 +592,44 @@ public void testBuildGrpcManagedChannelWithInsecureTlsAndCustomEndpoint() throws
// 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);
}
}
Loading