diff --git a/sdk/src/main/java/io/dapr/config/Properties.java b/sdk/src/main/java/io/dapr/config/Properties.java index 98cccf2c4..51eaf5d25 100644 --- a/sdk/src/main/java/io/dapr/config/Properties.java +++ b/sdk/src/main/java/io/dapr/config/Properties.java @@ -145,6 +145,38 @@ public class Properties { "DAPR_GRPC_ENDPOINT", null); + /** + * GRPC enable keep alive. + */ + public static final Property 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 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 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 GRPC_KEEP_ALIVE_WITHOUT_CALLS = new BooleanProperty( + "dapr.grpc.keepAliveWithoutCalls", + "DAPR_GRPC_KEEP_ALIVE_WITHOUT_CALLS", + true); + /** * GRPC endpoint for remote sidecar connectivity. */ diff --git a/sdk/src/main/java/io/dapr/config/SecondsDurationProperty.java b/sdk/src/main/java/io/dapr/config/SecondsDurationProperty.java new file mode 100644 index 000000000..02bf1f7ee --- /dev/null +++ b/sdk/src/main/java/io/dapr/config/SecondsDurationProperty.java @@ -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 { + + /** + * {@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); + } + +} diff --git a/sdk/src/main/java/io/dapr/utils/NetworkUtils.java b/sdk/src/main/java/io/dapr/utils/NetworkUtils.java index 522b3e5d7..431bed792 100644 --- a/sdk/src/main/java/io/dapr/utils/NetworkUtils.java +++ b/sdk/src/main/java/io/dapr/utils/NetworkUtils.java @@ -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 = - "(?dns://)(?" + private static final String GRPC_ENDPOINT_DNS_AUTHORITY_REGEX_PART = "(?dns://)" + + "(?" + GRPC_ENDPOINT_HOSTNAME_REGEX_PART + ":[0-9]+)?/"; private static final String GRPC_ENDPOINT_PARAM_REGEX_PART = "(\\?(?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); } } diff --git a/sdk/src/test/java/io/dapr/utils/NetworkUtilsTest.java b/sdk/src/test/java/io/dapr/utils/NetworkUtilsTest.java index 2b4929abd..a2eee4183 100644 --- a/sdk/src/test/java/io/dapr/utils/NetworkUtilsTest.java +++ b/sdk/src/test/java/io/dapr/utils/NetworkUtilsTest.java @@ -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); + } +} \ No newline at end of file