From bdca3b320bb9695e5caa2e68226cb930e8487d0e Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 8 Apr 2024 18:23:49 +0100 Subject: [PATCH] Adds an option to set a timeout for service invocation (#1252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds http timeout Signed-off-by: Elena Kolevska * Adds a timeout for the grpc client Signed-off-by: Elena Kolevska * Small updates Signed-off-by: Elena Kolevska * Updates test Signed-off-by: Elena Kolevska * Adds a timeout example in docs Signed-off-by: Elena Kolevska * Adds e2e test for http service invocation Signed-off-by: Elena Kolevska * Adds tests for grpc service invocation Signed-off-by: Elena Kolevska * Removes grpc timeout, because it’s not needed. It can be passed directly to the call as shown in the updated tests and docs Signed-off-by: Elena Kolevska * Update src/Dapr.Client/DaprClientBuilder.cs Signed-off-by: Elena Kolevska --------- Signed-off-by: Elena Kolevska Signed-off-by: Elena Kolevska Co-authored-by: Phillip Hoff --- .../dotnet-sdk-docs/dotnet-client/_index.md | 25 +++++++++++++++++-- src/Dapr.Client/DaprClientBuilder.cs | 21 +++++++++++++++- .../DaprClientBuilderTest.cs | 11 ++++++++ .../Proto/message.proto | 1 + .../Services/MessagerService.cs | 7 ++++++ .../Controllers/TestController.cs | 9 +++++++ .../E2ETests.GrpcProxyInvocationTests.cs | 17 +++++++++++++ .../E2ETests.ServiceInvocationTests.cs | 24 ++++++++++++++++++ 8 files changed, 112 insertions(+), 3 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md index 41e610125..f608cd07d 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -21,13 +21,16 @@ The .NET SDK allows you to interface with all of the [Dapr building blocks]({{< ### Invoke a service +#### HTTP You can either use the `DaprClient` or `System.Net.Http.HttpClient` to invoke your services. {{< tabs SDK HTTP>}} {{% codetab %}} ```csharp -using var client = new DaprClientBuilder().Build(); +using var client = new DaprClientBuilder(). + UseTimeout(TimeSpan.FromSeconds(2)). // Optionally, set a timeout + Build(); // Invokes a POST method named "deposit" that takes input of type "Transaction" var data = new { id = "17", amount = 99m }; @@ -40,15 +43,33 @@ Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance) ```csharp var client = DaprClient.CreateInvokeHttpClient(appId: "routing"); +// To set a timeout on the HTTP client: +client.Timeout = TimeSpan.FromSeconds(2); + var deposit = new Transaction { Id = "17", Amount = 99m }; var response = await client.PostAsJsonAsync("/deposit", deposit, cancellationToken); var account = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance); ``` {{% /codetab %}} - {{< /tabs >}} +#### gRPC +You can use the `DaprClient` to invoke your services over gRPC. +{{% codetab %}} +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); +var invoker = DaprClient.CreateInvocationInvoker(appId: myAppId, daprEndpoint: serviceEndpoint); +var client = new MyService.MyServiceClient(invoker); + +var options = new CallOptions(cancellationToken: cts.Token, deadline: DateTime.UtcNow.AddSeconds(1)); +await client.MyMethodAsync(new Empty(), options); + +Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode); +``` +{{% /codetab %}} + + - For a full guide on service invocation visit [How-To: Invoke a service]({{< ref howto-invoke-discover-services.md >}}). ### Save & get application state diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 1580afb36..50a4979d1 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -57,6 +57,7 @@ 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; } /// /// Overrides the HTTP endpoint used by for communicating with the Dapr runtime. @@ -136,6 +137,17 @@ public DaprClientBuilder UseDaprApiToken(string apiToken) return this; } + /// + /// Sets the timeout for the HTTP client used by the . + /// + /// + /// + public DaprClientBuilder UseTimeout(TimeSpan timeout) + { + this.Timeout = timeout; + return this; + } + /// /// Builds a instance from the properties of the builder. /// @@ -162,9 +174,16 @@ public DaprClient Build() 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; + } + return new DaprClientGrpc(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); } } diff --git a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs index 2da50922a..52d0b7000 100644 --- a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs @@ -14,6 +14,7 @@ using System; using System.Text.Json; using Dapr.Client; +using Grpc.Core; using Grpc.Net.Client; using Xunit; @@ -110,5 +111,15 @@ public void DaprClientBuilder_ApiTokenNotSet_EmptyApiTokenHeader() var entry = DaprClient.GetDaprApiTokenHeader(builder.DaprApiToken); Assert.Equal(default, entry); } + + [Fact] + public void DaprClientBuilder_SetsTimeout() + { + var builder = new DaprClientBuilder(); + builder.UseTimeout(TimeSpan.FromSeconds(2)); + builder.Build(); + Assert.Equal(2, builder.Timeout.Seconds); + } } + } diff --git a/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto b/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto index a8f67f578..3b73b73a9 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto +++ b/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto @@ -25,6 +25,7 @@ service Messager { rpc GetMessage(GetMessageRequest) returns (MessageResponse); // Send a series of broadcast messages. rpc StreamBroadcast(stream Broadcast) returns (stream MessageResponse); + rpc DelayedResponse(google.protobuf.Empty) returns (google.protobuf.Empty); } message SendMessageRequest { diff --git a/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs b/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs index 0e22f3eec..950668932 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs +++ b/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System; using System.Threading.Tasks; using Google.Protobuf.WellKnownTypes; using Grpc.Core; @@ -44,5 +45,11 @@ public override async Task StreamBroadcast(IAsyncStreamReader request await responseStream.WriteAsync(new MessageResponse { Message = request.Message }); } } + + public override async Task DelayedResponse(Empty request, ServerCallContext context) + { + await Task.Delay(TimeSpan.FromSeconds(2)); + return new Empty(); + } } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Controllers/TestController.cs b/test/Dapr.E2E.Test.App/Controllers/TestController.cs index 1f26ce809..4e475c197 100644 --- a/test/Dapr.E2E.Test.App/Controllers/TestController.cs +++ b/test/Dapr.E2E.Test.App/Controllers/TestController.cs @@ -65,5 +65,14 @@ public ActionResult AccountDetailsRequiresApiToken(Transaction transact }; return account; } + + [Authorize("Dapr")] + [HttpGet("DelayedResponse")] + public async Task DelayedResponse() + { + await Task.Delay(TimeSpan.FromSeconds(2)); + return Ok(); + } + } } diff --git a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs index 0f991a9c7..3b6f31e84 100644 --- a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs +++ b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs @@ -15,6 +15,7 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; +using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Xunit; using Xunit.Abstractions; @@ -77,5 +78,21 @@ public async Task TestGrpcProxyStreamingBroadcast() await responseTask; } } + + [Fact] + public async Task TestGrpcServiceInvocationWithTimeout() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + var invoker = DaprClient.CreateInvocationInvoker(appId: this.AppId, daprEndpoint: this.GrpcEndpoint); + var client = new Messager.MessagerClient(invoker); + + var options = new CallOptions(cancellationToken: cts.Token, deadline: DateTime.UtcNow.AddSeconds(1)); + var ex = await Assert.ThrowsAsync(async () => + { + await client.DelayedResponseAsync(new Empty(), options); + }); + + Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode); + } } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs index cdc6170ce..da49e6721 100644 --- a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs +++ b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs @@ -13,10 +13,15 @@ namespace Dapr.E2E.Test { using System; + using System.Net; + using System.Net.Http; using System.Net.Http.Json; + using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Dapr.Client; + using Google.Protobuf.WellKnownTypes; + using Grpc.Core; using Xunit; public partial class E2ETests @@ -58,6 +63,25 @@ public async Task TestServiceInvocationRequiresApiToken() Assert.Equal("1", account.Id); Assert.Equal(150, account.Balance); } + + [Fact] + public async Task TestHttpServiceInvocationWithTimeout() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + using var client = new DaprClientBuilder() + .UseHttpEndpoint(this.HttpEndpoint) + .UseTimeout(TimeSpan.FromSeconds(1)) + .Build(); + + await Assert.ThrowsAsync(async () => + { + await client.InvokeMethodAsync( + appId: this.AppId, + methodName: "DelayedResponse", + httpMethod: new HttpMethod("GET"), + cancellationToken: cts.Token); + }); + } } internal class Transaction