Skip to content

Commit 58ad730

Browse files
committed
Add grpc keep alive
Signed-off-by: Jonathan Collinge <[email protected]>
1 parent 326ad37 commit 58ad730

File tree

3 files changed

+221
-9
lines changed

3 files changed

+221
-9
lines changed

src/Dapr.Client/DaprClientBuilder.cs

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,13 @@ public DaprClientBuilder()
3232
this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint();
3333
this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint();
3434

35-
this.GrpcChannelOptions = new GrpcChannelOptions()
36-
{
35+
this.GrpcKeepAliveEnabled = DaprDefaults.GetDefaultGrpcKeepAliveEnable();
36+
this.GrpcKeepAliveTime = TimeSpan.FromSeconds(DaprDefaults.GetDefaultGrpcKeepAliveTimeSeconds());
37+
this.GrpcKeepAliveTimeout = TimeSpan.FromSeconds(DaprDefaults.GetDefaultGrpcKeepAliveTimeoutSeconds());
38+
this.GrpcKeepAlivePermitWithoutCalls = DaprDefaults.GetDefaultGrpcKeepAliveWithoutCalls();
39+
40+
this.GrpcChannelOptions = new GrpcChannelOptions
41+
{
3742
// The gRPC client doesn't throw the right exception for cancellation
3843
// by default, this switches that behavior on.
3944
ThrowOperationCanceledOnCancellation = true,
@@ -57,7 +62,12 @@ public DaprClientBuilder()
5762
// property exposed for testing purposes
5863
internal GrpcChannelOptions GrpcChannelOptions { get; private set; }
5964
internal string DaprApiToken { get; private set; }
60-
internal TimeSpan Timeout { get; private set; }
65+
internal TimeSpan Timeout { get; private set; }
66+
67+
internal bool GrpcKeepAliveEnabled { get; private set; }
68+
internal TimeSpan GrpcKeepAliveTime { get; private set; }
69+
internal TimeSpan GrpcKeepAliveTimeout { get; private set; }
70+
internal bool GrpcKeepAlivePermitWithoutCalls { get; private set; }
6171

6272
/// <summary>
6373
/// Overrides the HTTP endpoint used by <see cref="DaprClient" /> for communicating with the Dapr runtime.
@@ -148,6 +158,50 @@ public DaprClientBuilder UseTimeout(TimeSpan timeout)
148158
return this;
149159
}
150160

161+
/// <summary>
162+
/// Enables or disables gRPC keep-alive.
163+
/// </summary>
164+
/// <param name="enabled">Whether to enable gRPC keep-alive.</param>
165+
/// <returns>The <see cref="DaprClientBuilder" /> instance.</returns>
166+
public DaprClientBuilder UseGrpcKeepAlive(bool enabled)
167+
{
168+
this.GrpcKeepAliveEnabled = enabled;
169+
return this;
170+
}
171+
172+
/// <summary>
173+
/// Sets the gRPC keep-alive time interval.
174+
/// </summary>
175+
/// <param name="keepAliveTime">The time interval between keep-alive pings.</param>
176+
/// <returns>The <see cref="DaprClientBuilder" /> instance.</returns>
177+
public DaprClientBuilder UseGrpcKeepAliveTime(TimeSpan keepAliveTime)
178+
{
179+
this.GrpcKeepAliveTime = keepAliveTime;
180+
return this;
181+
}
182+
183+
/// <summary>
184+
/// Sets the gRPC keep-alive timeout.
185+
/// </summary>
186+
/// <param name="keepAliveTimeout">The time to wait for a keep-alive ping response before considering the connection dead.</param>
187+
/// <returns>The <see cref="DaprClientBuilder" /> instance.</returns>
188+
public DaprClientBuilder UseGrpcKeepAliveTimeout(TimeSpan keepAliveTimeout)
189+
{
190+
this.GrpcKeepAliveTimeout = keepAliveTimeout;
191+
return this;
192+
}
193+
194+
/// <summary>
195+
/// Sets whether gRPC keep-alive should be sent when there are no active calls.
196+
/// </summary>
197+
/// <param name="permitWithoutCalls">Whether to send keep-alive pings even when there are no active calls.</param>
198+
/// <returns>The <see cref="DaprClientBuilder" /> instance.</returns>
199+
public DaprClientBuilder UseGrpcKeepAlivePermitWithoutCalls(bool permitWithoutCalls)
200+
{
201+
this.GrpcKeepAlivePermitWithoutCalls = permitWithoutCalls;
202+
return this;
203+
}
204+
151205
/// <summary>
152206
/// Builds a <see cref="DaprClient" /> instance from the properties of the builder.
153207
/// </summary>
@@ -172,13 +226,30 @@ public DaprClient Build()
172226
throw new InvalidOperationException("The HTTP endpoint must use http or https.");
173227
}
174228

229+
if (this.GrpcKeepAliveEnabled)
230+
{
231+
if (!(this.GrpcChannelOptions.HttpHandler is SocketsHttpHandler))
232+
{
233+
var handler = new SocketsHttpHandler();
234+
this.GrpcChannelOptions.HttpHandler = handler;
235+
}
236+
237+
var socketsHandler = (SocketsHttpHandler)this.GrpcChannelOptions.HttpHandler;
238+
239+
socketsHandler.KeepAlivePingDelay = this.GrpcKeepAliveTime;
240+
socketsHandler.KeepAlivePingTimeout = this.GrpcKeepAliveTimeout;
241+
socketsHandler.KeepAlivePingPolicy = this.GrpcKeepAlivePermitWithoutCalls
242+
? HttpKeepAlivePingPolicy.Always
243+
: HttpKeepAlivePingPolicy.WithActiveRequests;
244+
}
245+
175246
var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions);
176247
var client = new Autogenerated.Dapr.DaprClient(channel);
177248

178249

179250
var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken);
180251
var httpClient = HttpClientFactory is object ? HttpClientFactory() : new HttpClient();
181-
252+
182253
if (this.Timeout > TimeSpan.Zero)
183254
{
184255
httpClient.Timeout = this.Timeout;

src/Dapr.Common/DaprDefaults.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,19 @@ internal static class DaprDefaults
2828
public const string DaprHttpPortName = "DAPR_HTTP_PORT";
2929
public const string DaprGrpcEndpointName = "DAPR_GRPC_ENDPOINT";
3030
public const string DaprGrpcPortName = "DAPR_GRPC_PORT";
31+
public const string DaprGrpcKeepAliveEnableName = "DAPR_ENABLE_KEEP_ALIVE";
32+
public const string DaprGrpcKeepAliveTimeName = "DAPR_KEEP_ALIVE_TIME";
33+
public const string DaprGrpcKeepAliveTimeoutName = "DAPR_KEEP_ALIVE_TIMEOUT";
34+
public const string DaprGrpcKeepAliveWithoutCallsName = "DAPR_KEEP_ALIVE_WITHOUT_CALLS";
3135

3236
public const string DefaultDaprScheme = "http";
3337
public const string DefaultDaprHost = "localhost";
3438
public const int DefaultHttpPort = 3500;
3539
public const int DefaultGrpcPort = 50001;
40+
public const bool DefaultGrpcKeepAliveEnable = false;
41+
public const int DefaultGrpcKeepAliveTimeSeconds = 60;
42+
public const int DefaultGrpcKeepAliveTimeoutSeconds = 20;
43+
public const bool DefaultGrpcKeepAliveWithoutCalls = true;
3644

3745
/// <summary>
3846
/// Get the value of environment variable DAPR_API_TOKEN
@@ -130,4 +138,48 @@ private static string BuildEndpoint(string? endpoint, int endpointPort)
130138
//Fall back to the environment variable with the same name or default to an empty string
131139
return Environment.GetEnvironmentVariable(name);
132140
}
141+
142+
/// <summary>
143+
/// Get whether gRPC keep-alive is enabled based on environment variables.
144+
/// </summary>
145+
/// <param name="configuration">The optional <see cref="IConfiguration"/> to pull the value from.</param>
146+
/// <returns>A boolean indicating whether gRPC keep-alive is enabled.</returns>
147+
public static bool GetDefaultGrpcKeepAliveEnable(IConfiguration? configuration = null)
148+
{
149+
var value = GetResourceValue(configuration, DaprGrpcKeepAliveEnableName);
150+
return string.IsNullOrWhiteSpace(value) ? DefaultGrpcKeepAliveEnable : bool.Parse(value);
151+
}
152+
153+
/// <summary>
154+
/// Get the gRPC keep-alive time in seconds based on environment variables.
155+
/// </summary>
156+
/// <param name="configuration">The optional <see cref="IConfiguration"/> to pull the value from.</param>
157+
/// <returns>The gRPC keep-alive time in seconds.</returns>
158+
public static int GetDefaultGrpcKeepAliveTimeSeconds(IConfiguration? configuration = null)
159+
{
160+
var value = GetResourceValue(configuration, DaprGrpcKeepAliveTimeName);
161+
return string.IsNullOrWhiteSpace(value) ? DefaultGrpcKeepAliveTimeSeconds : int.Parse(value);
162+
}
163+
164+
/// <summary>
165+
/// Get the gRPC keep-alive timeout in seconds based on environment variables.
166+
/// </summary>
167+
/// <param name="configuration">The optional <see cref="IConfiguration"/> to pull the value from.</param>
168+
/// <returns>The gRPC keep-alive timeout in seconds.</returns>
169+
public static int GetDefaultGrpcKeepAliveTimeoutSeconds(IConfiguration? configuration = null)
170+
{
171+
var value = GetResourceValue(configuration, DaprGrpcKeepAliveTimeoutName);
172+
return string.IsNullOrWhiteSpace(value) ? DefaultGrpcKeepAliveTimeoutSeconds : int.Parse(value);
173+
}
174+
175+
/// <summary>
176+
/// Get whether gRPC keep-alive should be sent without calls based on environment variables.
177+
/// </summary>
178+
/// <param name="configuration">The optional <see cref="IConfiguration"/> to pull the value from.</param>
179+
/// <returns>A boolean indicating whether gRPC keep-alive should be sent without calls.</returns>
180+
public static bool GetDefaultGrpcKeepAliveWithoutCalls(IConfiguration? configuration = null)
181+
{
182+
var value = GetResourceValue(configuration, DaprGrpcKeepAliveWithoutCallsName);
183+
return string.IsNullOrWhiteSpace(value) ? DefaultGrpcKeepAliveWithoutCalls : bool.Parse(value);
184+
}
133185
}

src/Dapr.Common/DaprGenericClientBuilder.cs

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
// limitations under the License.
1212
// ------------------------------------------------------------------------
1313

14+
using System;
15+
using System;
1416
using System.Reflection;
1517
using System.Text.Json;
18+
using System.Threading;
1619
using Grpc.Net.Client;
1720
using Microsoft.Extensions.Configuration;
1821

@@ -31,6 +34,11 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null)
3134
this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint();
3235
this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint();
3336

37+
this.GrpcKeepAliveEnabled = DaprDefaults.GetDefaultGrpcKeepAliveEnable(configuration);
38+
this.GrpcKeepAliveTime = TimeSpan.FromSeconds(DaprDefaults.GetDefaultGrpcKeepAliveTimeSeconds(configuration));
39+
this.GrpcKeepAliveTimeout = TimeSpan.FromSeconds(DaprDefaults.GetDefaultGrpcKeepAliveTimeoutSeconds(configuration));
40+
this.GrpcKeepAlivePermitWithoutCalls = DaprDefaults.GetDefaultGrpcKeepAliveWithoutCalls(configuration);
41+
3442
this.GrpcChannelOptions = new GrpcChannelOptions()
3543
{
3644
// The gRPC client doesn't throw the right exception for cancellation
@@ -71,12 +79,32 @@ protected DaprGenericClientBuilder(IConfiguration? configuration = null)
7179
/// Property exposed for testing purposes.
7280
/// </summary>
7381
public string DaprApiToken { get; private set; }
74-
82+
7583
/// <summary>
7684
/// Property exposed for testing purposes.
7785
/// </summary>
7886
internal TimeSpan Timeout { get; private set; }
7987

88+
/// <summary>
89+
/// Property exposed for testing purposes.
90+
/// </summary>
91+
internal bool GrpcKeepAliveEnabled { get; private set; }
92+
93+
/// <summary>
94+
/// Property exposed for testing purposes.
95+
/// </summary>
96+
internal TimeSpan GrpcKeepAliveTime { get; private set; }
97+
98+
/// <summary>
99+
/// Property exposed for testing purposes.
100+
/// </summary>
101+
internal TimeSpan GrpcKeepAliveTimeout { get; private set; }
102+
103+
/// <summary>
104+
/// Property exposed for testing purposes.
105+
/// </summary>
106+
internal bool GrpcKeepAlivePermitWithoutCalls { get; private set; }
107+
80108
/// <summary>
81109
/// Overrides the HTTP endpoint used by the Dapr client for communicating with the Dapr runtime.
82110
/// </summary>
@@ -180,6 +208,50 @@ public DaprGenericClientBuilder<TClientBuilder> UseTimeout(TimeSpan timeout)
180208
return this;
181209
}
182210

211+
/// <summary>
212+
/// Enables or disables gRPC keep-alive.
213+
/// </summary>
214+
/// <param name="enabled">Whether to enable gRPC keep-alive.</param>
215+
/// <returns>The <see cref="DaprGenericClientBuilder{TClientBuilder}" /> instance.</returns>
216+
public DaprGenericClientBuilder<TClientBuilder> UseGrpcKeepAlive(bool enabled)
217+
{
218+
this.GrpcKeepAliveEnabled = enabled;
219+
return this;
220+
}
221+
222+
/// <summary>
223+
/// Sets the gRPC keep-alive time interval.
224+
/// </summary>
225+
/// <param name="keepAliveTime">The time interval between keep-alive pings.</param>
226+
/// <returns>The <see cref="DaprGenericClientBuilder{TClientBuilder}" /> instance.</returns>
227+
public DaprGenericClientBuilder<TClientBuilder> UseGrpcKeepAliveTime(TimeSpan keepAliveTime)
228+
{
229+
this.GrpcKeepAliveTime = keepAliveTime;
230+
return this;
231+
}
232+
233+
/// <summary>
234+
/// Sets the gRPC keep-alive timeout.
235+
/// </summary>
236+
/// <param name="keepAliveTimeout">The time to wait for a keep-alive ping response before considering the connection dead.</param>
237+
/// <returns>The <see cref="DaprGenericClientBuilder{TClientBuilder}" /> instance.</returns>
238+
public DaprGenericClientBuilder<TClientBuilder> UseGrpcKeepAliveTimeout(TimeSpan keepAliveTimeout)
239+
{
240+
this.GrpcKeepAliveTimeout = keepAliveTimeout;
241+
return this;
242+
}
243+
244+
/// <summary>
245+
/// Sets whether gRPC keep-alive should be sent when there are no active calls.
246+
/// </summary>
247+
/// <param name="permitWithoutCalls">Whether to send keep-alive pings even when there are no active calls.</param>
248+
/// <returns>The <see cref="DaprGenericClientBuilder{TClientBuilder}" /> instance.</returns>
249+
public DaprGenericClientBuilder<TClientBuilder> UseGrpcKeepAlivePermitWithoutCalls(bool permitWithoutCalls)
250+
{
251+
this.GrpcKeepAlivePermitWithoutCalls = permitWithoutCalls;
252+
return this;
253+
}
254+
183255
/// <summary>
184256
/// Builds out the inner DaprClient that provides the core shape of the
185257
/// runtime gRPC client used by the consuming package.
@@ -209,8 +281,25 @@ protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint
209281
//Configure the HTTP client
210282
var httpClient = ConfigureHttpClient(assembly);
211283
this.GrpcChannelOptions.HttpClient = httpClient;
284+
285+
if (this.GrpcKeepAliveEnabled)
286+
{
287+
if (!(this.GrpcChannelOptions.HttpHandler is SocketsHttpHandler))
288+
{
289+
var handler = new SocketsHttpHandler();
290+
this.GrpcChannelOptions.HttpHandler = handler;
291+
}
292+
293+
var socketsHandler = (SocketsHttpHandler)this.GrpcChannelOptions.HttpHandler;
294+
295+
socketsHandler.KeepAlivePingDelay = this.GrpcKeepAliveTime;
296+
socketsHandler.KeepAlivePingTimeout = this.GrpcKeepAliveTimeout;
297+
socketsHandler.KeepAlivePingPolicy = this.GrpcKeepAlivePermitWithoutCalls
298+
? HttpKeepAlivePingPolicy.Always
299+
: HttpKeepAlivePingPolicy.WithActiveRequests;
300+
}
212301

213-
var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions);
302+
var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions);
214303
return (channel, httpClient, httpEndpoint, this.DaprApiToken);
215304
}
216305

@@ -222,17 +311,17 @@ protected internal (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint
222311
private HttpClient ConfigureHttpClient(Assembly assembly)
223312
{
224313
var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient();
225-
314+
226315
//Set the timeout as necessary
227316
if (this.Timeout > TimeSpan.Zero)
228317
{
229318
httpClient.Timeout = this.Timeout;
230319
}
231-
320+
232321
//Set the user agent
233322
var userAgent = DaprClientUtilities.GetUserAgent(assembly);
234323
httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent.ToString());
235-
324+
236325
//Set the API token
237326
var apiTokenHeader = DaprClientUtilities.GetDaprApiTokenHeader(this.DaprApiToken);
238327
if (apiTokenHeader is not null)

0 commit comments

Comments
 (0)