diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index ad3fe6b0f..6f9118c54 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -32,8 +32,13 @@ public DaprClientBuilder() this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); - this.GrpcChannelOptions = new GrpcChannelOptions() - { + this.GrpcKeepAliveEnabled = DaprDefaults.GetDefaultGrpcKeepAliveEnable(); + this.GrpcKeepAliveTime = TimeSpan.FromSeconds(DaprDefaults.GetDefaultGrpcKeepAliveTimeSeconds()); + this.GrpcKeepAliveTimeout = TimeSpan.FromSeconds(DaprDefaults.GetDefaultGrpcKeepAliveTimeoutSeconds()); + this.GrpcKeepAlivePermitWithoutCalls = DaprDefaults.GetDefaultGrpcKeepAliveWithoutCalls(); + + this.GrpcChannelOptions = new GrpcChannelOptions + { // The gRPC client doesn't throw the right exception for cancellation // by default, this switches that behavior on. ThrowOperationCanceledOnCancellation = true, @@ -57,7 +62,12 @@ public DaprClientBuilder() // property exposed for testing purposes internal GrpcChannelOptions GrpcChannelOptions { get; private set; } internal string DaprApiToken { get; private set; } - internal TimeSpan Timeout { get; private set; } + internal TimeSpan Timeout { get; private set; } + + internal bool GrpcKeepAliveEnabled { get; private set; } + internal TimeSpan GrpcKeepAliveTime { get; private set; } + internal TimeSpan GrpcKeepAliveTimeout { get; private set; } + internal bool GrpcKeepAlivePermitWithoutCalls { get; private set; } /// /// Overrides the HTTP endpoint used by for communicating with the Dapr runtime. @@ -148,6 +158,50 @@ public DaprClientBuilder UseTimeout(TimeSpan timeout) return this; } + /// + /// Enables or disables gRPC keep-alive. + /// + /// Whether to enable gRPC keep-alive. + /// The instance. + public DaprClientBuilder UseGrpcKeepAlive(bool enabled) + { + this.GrpcKeepAliveEnabled = enabled; + return this; + } + + /// + /// Sets the gRPC keep-alive time interval. + /// + /// The time interval between keep-alive pings. + /// The instance. + public DaprClientBuilder UseGrpcKeepAliveTime(TimeSpan keepAliveTime) + { + this.GrpcKeepAliveTime = keepAliveTime; + return this; + } + + /// + /// Sets the gRPC keep-alive timeout. + /// + /// The time to wait for a keep-alive ping response before considering the connection dead. + /// The instance. + public DaprClientBuilder UseGrpcKeepAliveTimeout(TimeSpan keepAliveTimeout) + { + this.GrpcKeepAliveTimeout = keepAliveTimeout; + return this; + } + + /// + /// Sets whether gRPC keep-alive should be sent when there are no active calls. + /// + /// Whether to send keep-alive pings even when there are no active calls. + /// The instance. + public DaprClientBuilder UseGrpcKeepAlivePermitWithoutCalls(bool permitWithoutCalls) + { + this.GrpcKeepAlivePermitWithoutCalls = permitWithoutCalls; + return this; + } + /// /// Builds a instance from the properties of the builder. /// @@ -172,13 +226,30 @@ public DaprClient Build() throw new InvalidOperationException("The HTTP endpoint must use http or https."); } + if (this.GrpcKeepAliveEnabled) + { + if (!(this.GrpcChannelOptions.HttpHandler is SocketsHttpHandler)) + { + var handler = new SocketsHttpHandler(); + this.GrpcChannelOptions.HttpHandler = handler; + } + + var socketsHandler = (SocketsHttpHandler)this.GrpcChannelOptions.HttpHandler; + + socketsHandler.KeepAlivePingDelay = this.GrpcKeepAliveTime; + socketsHandler.KeepAlivePingTimeout = this.GrpcKeepAliveTimeout; + socketsHandler.KeepAlivePingPolicy = this.GrpcKeepAlivePermitWithoutCalls + ? HttpKeepAlivePingPolicy.Always + : HttpKeepAlivePingPolicy.WithActiveRequests; + } + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); var client = new Autogenerated.Dapr.DaprClient(channel); var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken); var httpClient = HttpClientFactory is object ? HttpClientFactory() : new HttpClient(); - + if (this.Timeout > TimeSpan.Zero) { httpClient.Timeout = this.Timeout; diff --git a/src/Dapr.Common/DaprDefaults.cs b/src/Dapr.Common/DaprDefaults.cs index 9a504986d..2f842421d 100644 --- a/src/Dapr.Common/DaprDefaults.cs +++ b/src/Dapr.Common/DaprDefaults.cs @@ -28,11 +28,19 @@ internal static class DaprDefaults public const string DaprHttpPortName = "DAPR_HTTP_PORT"; public const string DaprGrpcEndpointName = "DAPR_GRPC_ENDPOINT"; public const string DaprGrpcPortName = "DAPR_GRPC_PORT"; + public const string DaprGrpcKeepAliveEnableName = "DAPR_ENABLE_KEEP_ALIVE"; + public const string DaprGrpcKeepAliveTimeName = "DAPR_KEEP_ALIVE_TIME"; + public const string DaprGrpcKeepAliveTimeoutName = "DAPR_KEEP_ALIVE_TIMEOUT"; + public const string DaprGrpcKeepAliveWithoutCallsName = "DAPR_KEEP_ALIVE_WITHOUT_CALLS"; public const string DefaultDaprScheme = "http"; public const string DefaultDaprHost = "localhost"; public const int DefaultHttpPort = 3500; public const int DefaultGrpcPort = 50001; + public const bool DefaultGrpcKeepAliveEnable = false; + public const int DefaultGrpcKeepAliveTimeSeconds = 60; + public const int DefaultGrpcKeepAliveTimeoutSeconds = 20; + public const bool DefaultGrpcKeepAliveWithoutCalls = true; /// /// Get the value of environment variable DAPR_API_TOKEN @@ -130,4 +138,48 @@ private static string BuildEndpoint(string? endpoint, int endpointPort) //Fall back to the environment variable with the same name or default to an empty string return Environment.GetEnvironmentVariable(name); } + + /// + /// Get whether gRPC keep-alive is enabled based on environment variables. + /// + /// The optional to pull the value from. + /// A boolean indicating whether gRPC keep-alive is enabled. + public static bool GetDefaultGrpcKeepAliveEnable(IConfiguration? configuration = null) + { + var value = GetResourceValue(configuration, DaprGrpcKeepAliveEnableName); + return string.IsNullOrWhiteSpace(value) ? DefaultGrpcKeepAliveEnable : bool.Parse(value); + } + + /// + /// Get the gRPC keep-alive time in seconds based on environment variables. + /// + /// The optional to pull the value from. + /// The gRPC keep-alive time in seconds. + public static int GetDefaultGrpcKeepAliveTimeSeconds(IConfiguration? configuration = null) + { + var value = GetResourceValue(configuration, DaprGrpcKeepAliveTimeName); + return string.IsNullOrWhiteSpace(value) ? DefaultGrpcKeepAliveTimeSeconds : int.Parse(value); + } + + /// + /// Get the gRPC keep-alive timeout in seconds based on environment variables. + /// + /// The optional to pull the value from. + /// The gRPC keep-alive timeout in seconds. + public static int GetDefaultGrpcKeepAliveTimeoutSeconds(IConfiguration? configuration = null) + { + var value = GetResourceValue(configuration, DaprGrpcKeepAliveTimeoutName); + return string.IsNullOrWhiteSpace(value) ? DefaultGrpcKeepAliveTimeoutSeconds : int.Parse(value); + } + + /// + /// Get whether gRPC keep-alive should be sent without calls based on environment variables. + /// + /// The optional to pull the value from. + /// A boolean indicating whether gRPC keep-alive should be sent without calls. + public static bool GetDefaultGrpcKeepAliveWithoutCalls(IConfiguration? configuration = null) + { + var value = GetResourceValue(configuration, DaprGrpcKeepAliveWithoutCallsName); + return string.IsNullOrWhiteSpace(value) ? DefaultGrpcKeepAliveWithoutCalls : bool.Parse(value); + } } diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 3e29a2eff..cf71fa022 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -11,8 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System; +using System; using System.Reflection; using System.Text.Json; +using System.Threading; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; @@ -31,6 +34,11 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null) this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); + this.GrpcKeepAliveEnabled = DaprDefaults.GetDefaultGrpcKeepAliveEnable(configuration); + this.GrpcKeepAliveTime = TimeSpan.FromSeconds(DaprDefaults.GetDefaultGrpcKeepAliveTimeSeconds(configuration)); + this.GrpcKeepAliveTimeout = TimeSpan.FromSeconds(DaprDefaults.GetDefaultGrpcKeepAliveTimeoutSeconds(configuration)); + this.GrpcKeepAlivePermitWithoutCalls = DaprDefaults.GetDefaultGrpcKeepAliveWithoutCalls(configuration); + this.GrpcChannelOptions = new GrpcChannelOptions() { // The gRPC client doesn't throw the right exception for cancellation @@ -71,12 +79,32 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null) /// Property exposed for testing purposes. /// public string DaprApiToken { get; private set; } - + /// /// Property exposed for testing purposes. /// internal TimeSpan Timeout { get; private set; } + /// + /// Property exposed for testing purposes. + /// + internal bool GrpcKeepAliveEnabled { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal TimeSpan GrpcKeepAliveTime { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal TimeSpan GrpcKeepAliveTimeout { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal bool GrpcKeepAlivePermitWithoutCalls { get; private set; } + /// /// Overrides the HTTP endpoint used by the Dapr client for communicating with the Dapr runtime. /// @@ -180,6 +208,50 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) return this; } + /// + /// Enables or disables gRPC keep-alive. + /// + /// Whether to enable gRPC keep-alive. + /// The instance. + public DaprGenericClientBuilder UseGrpcKeepAlive(bool enabled) + { + this.GrpcKeepAliveEnabled = enabled; + return this; + } + + /// + /// Sets the gRPC keep-alive time interval. + /// + /// The time interval between keep-alive pings. + /// The instance. + public DaprGenericClientBuilder UseGrpcKeepAliveTime(TimeSpan keepAliveTime) + { + this.GrpcKeepAliveTime = keepAliveTime; + return this; + } + + /// + /// Sets the gRPC keep-alive timeout. + /// + /// The time to wait for a keep-alive ping response before considering the connection dead. + /// The instance. + public DaprGenericClientBuilder UseGrpcKeepAliveTimeout(TimeSpan keepAliveTimeout) + { + this.GrpcKeepAliveTimeout = keepAliveTimeout; + return this; + } + + /// + /// Sets whether gRPC keep-alive should be sent when there are no active calls. + /// + /// Whether to send keep-alive pings even when there are no active calls. + /// The instance. + public DaprGenericClientBuilder UseGrpcKeepAlivePermitWithoutCalls(bool permitWithoutCalls) + { + this.GrpcKeepAlivePermitWithoutCalls = permitWithoutCalls; + return this; + } + /// /// Builds out the inner DaprClient that provides the core shape of the /// runtime gRPC client used by the consuming package. @@ -209,8 +281,25 @@ protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint //Configure the HTTP client var httpClient = ConfigureHttpClient(assembly); this.GrpcChannelOptions.HttpClient = httpClient; + + if (this.GrpcKeepAliveEnabled) + { + if (!(this.GrpcChannelOptions.HttpHandler is SocketsHttpHandler)) + { + var handler = new SocketsHttpHandler(); + this.GrpcChannelOptions.HttpHandler = handler; + } + + var socketsHandler = (SocketsHttpHandler)this.GrpcChannelOptions.HttpHandler; + + socketsHandler.KeepAlivePingDelay = this.GrpcKeepAliveTime; + socketsHandler.KeepAlivePingTimeout = this.GrpcKeepAliveTimeout; + socketsHandler.KeepAlivePingPolicy = this.GrpcKeepAlivePermitWithoutCalls + ? HttpKeepAlivePingPolicy.Always + : HttpKeepAlivePingPolicy.WithActiveRequests; + } - var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); return (channel, httpClient, httpEndpoint, this.DaprApiToken); } @@ -222,17 +311,17 @@ protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint private HttpClient ConfigureHttpClient(Assembly assembly) { var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); - + //Set the timeout as necessary if (this.Timeout > TimeSpan.Zero) { httpClient.Timeout = this.Timeout; } - + //Set the user agent var userAgent = DaprClientUtilities.GetUserAgent(assembly); httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent.ToString()); - + //Set the API token var apiTokenHeader = DaprClientUtilities.GetDaprApiTokenHeader(this.DaprApiToken); if (apiTokenHeader is not null)