From 560e141e68e34df0c061f91ee7c9de74853c47c8 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 7 May 2021 12:09:47 +1200 Subject: [PATCH] Add retrier example app (#1284) --- examples/README.md | 8 ++ examples/Retrier/Client/Client.csproj | 16 +++ examples/Retrier/Client/Program.cs | 125 ++++++++++++++++++ examples/Retrier/Proto/retry.proto | 31 +++++ examples/Retrier/Retrier.sln | 31 +++++ examples/Retrier/Server/Program.cs | 38 ++++++ examples/Retrier/Server/Server.csproj | 13 ++ .../Retrier/Server/Services/RetrierService.cs | 44 ++++++ examples/Retrier/Server/Startup.cs | 48 +++++++ .../Server/appsettings.Development.json | 10 ++ examples/Retrier/Server/appsettings.json | 13 ++ 11 files changed, 377 insertions(+) create mode 100644 examples/Retrier/Client/Client.csproj create mode 100644 examples/Retrier/Client/Program.cs create mode 100644 examples/Retrier/Proto/retry.proto create mode 100644 examples/Retrier/Retrier.sln create mode 100644 examples/Retrier/Server/Program.cs create mode 100644 examples/Retrier/Server/Server.csproj create mode 100644 examples/Retrier/Server/Services/RetrierService.cs create mode 100644 examples/Retrier/Server/Startup.cs create mode 100644 examples/Retrier/Server/appsettings.Development.json create mode 100644 examples/Retrier/Server/appsettings.json diff --git a/examples/README.md b/examples/README.md index 49a8f233a..d95ef71e3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -260,3 +260,11 @@ Code-first is a good choice if an app is written entirely in .NET. Code contract * Configure [protobuf-net.Grpc](https://github.com/protobuf-net/protobuf-net.Grpc) * Create a code-first gRPC service * Create a code-first gRPC client + +## [Retrier](./Retrier) + +The retrier example shows how to configure a client to use gRPC retries to retry failed calls. gRPC retries enables resilient, fault tolerant gRPC apps in .NET. + +##### Scenarios: + +* Configure [gRPC retires](https://docs.microsoft.com/aspnet/core/grpc/retries) diff --git a/examples/Retrier/Client/Client.csproj b/examples/Retrier/Client/Client.csproj new file mode 100644 index 000000000..15cbb932b --- /dev/null +++ b/examples/Retrier/Client/Client.csproj @@ -0,0 +1,16 @@ + + + + Exe + net5.0 + + + + + + + + + + + diff --git a/examples/Retrier/Client/Program.cs b/examples/Retrier/Client/Program.cs new file mode 100644 index 000000000..224b5c8a9 --- /dev/null +++ b/examples/Retrier/Client/Program.cs @@ -0,0 +1,125 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC 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. + +#endregion + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Net.Client; +using Grpc.Net.Client.Configuration; +using Retry; + +namespace Client +{ + public class Program + { + static async Task Main(string[] args) + { + using var channel = CreateChannel(); + var client = new Retrier.RetrierClient(channel); + + await UnaryRetry(client); + + Console.WriteLine("Shutting down"); + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(); + } + + private static async Task UnaryRetry(Retrier.RetrierClient client) + { + Console.WriteLine("Delivering packages..."); + foreach (var product in Products) + { + try + { + var package = new Package { Name = product }; + var call = client.DeliverPackageAsync(package); + var response = await call; + + #region Print success + Console.ForegroundColor = ConsoleColor.Green; + Console.Write(response.Message); + Console.ResetColor(); + Console.Write(" " + await GetRetryCount(call.ResponseHeadersAsync)); + Console.WriteLine(); + #endregion + } + catch (RpcException ex) + { + #region Print failure + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(ex.Status.Detail); + Console.ResetColor(); + #endregion + } + + await Task.Delay(TimeSpan.FromSeconds(0.2)); + } + } + + private static GrpcChannel CreateChannel() + { + var methodConfig = new MethodConfig + { + Names = { MethodName.Default }, + RetryPolicy = new RetryPolicy + { + MaxAttempts = 10, + InitialBackoff = TimeSpan.FromSeconds(0.5), + MaxBackoff = TimeSpan.FromSeconds(0.5), + BackoffMultiplier = 1, + RetryableStatusCodes = { StatusCode.Unavailable } + } + }; + + return GrpcChannel.ForAddress("http://localhost:5000", new GrpcChannelOptions + { + ServiceConfig = new ServiceConfig { MethodConfigs = { methodConfig } } + }); + } + + private static async Task GetRetryCount(Task responseHeadersTask) + { + var headers = await responseHeadersTask; + var previousAttemptCount = headers.GetValue("grpc-previous-rpc-attempts"); + return previousAttemptCount != null ? $"(retry count: {previousAttemptCount})" : string.Empty; + } + + private static readonly IList Products = new List + { + "Secrets of Silicon Valley", + "The Busy Executive's Database Guide", + "Emotional Security: A New Algorithm", + "Prolonged Data Deprivation: Four Case Studies", + "Cooking with Computers: Surreptitious Balance Sheets", + "Silicon Valley Gastronomic Treats", + "Sushi, Anyone?", + "Fifty Years in Buckingham Palace Kitchens", + "But Is It User Friendly?", + "You Can Combat Computer Stress!", + "Is Anger the Enemy?", + "Life Without Fear", + "The Gourmet Microwave", + "Onions, Leeks, and Garlic: Cooking Secrets of the Mediterranean", + "The Psychology of Computer Cooking", + "Straight Talk About Computers", + "Computer Phobic AND Non-Phobic Individuals: Behavior Variations", + "Net Etiquette" + }; + } +} diff --git a/examples/Retrier/Proto/retry.proto b/examples/Retrier/Proto/retry.proto new file mode 100644 index 000000000..abfd07641 --- /dev/null +++ b/examples/Retrier/Proto/retry.proto @@ -0,0 +1,31 @@ +// Copyright 2019 The gRPC 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. + +syntax = "proto3"; + +import "google/protobuf/wrappers.proto"; + +package retry; + +service Retrier { + rpc DeliverPackage (Package) returns (Response); +} + +message Package { + string name = 1; +} + +message Response { + string message = 1; +} diff --git a/examples/Retrier/Retrier.sln b/examples/Retrier/Retrier.sln new file mode 100644 index 000000000..37e6a3d07 --- /dev/null +++ b/examples/Retrier/Retrier.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29230.61 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Server\Server.csproj", "{534AC5F8-2DF2-40BD-87A5-B3D8310118C4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{48A1D3BC-A14B-436A-8822-6DE2BEF8B747}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {534AC5F8-2DF2-40BD-87A5-B3D8310118C4}.Release|Any CPU.Build.0 = Release|Any CPU + {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48A1D3BC-A14B-436A-8822-6DE2BEF8B747}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D22B3129-3BFB-41FA-9FCE-E45EBEF8C2DD} + EndGlobalSection +EndGlobal diff --git a/examples/Retrier/Server/Program.cs b/examples/Retrier/Server/Program.cs new file mode 100644 index 000000000..8ec497bbf --- /dev/null +++ b/examples/Retrier/Server/Program.cs @@ -0,0 +1,38 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC 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. + +#endregion + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Server +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/examples/Retrier/Server/Server.csproj b/examples/Retrier/Server/Server.csproj new file mode 100644 index 000000000..63e9d3525 --- /dev/null +++ b/examples/Retrier/Server/Server.csproj @@ -0,0 +1,13 @@ + + + + net5.0 + + + + + + + + + diff --git a/examples/Retrier/Server/Services/RetrierService.cs b/examples/Retrier/Server/Services/RetrierService.cs new file mode 100644 index 000000000..46b7e0796 --- /dev/null +++ b/examples/Retrier/Server/Services/RetrierService.cs @@ -0,0 +1,44 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC 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. + +#endregion + +using System; +using System.Threading.Tasks; +using Grpc.Core; +using Retry; + +namespace Server +{ + public class RetrierService : Retrier.RetrierBase + { + private readonly Random _random = new Random(); + + public override Task DeliverPackage(Package request, ServerCallContext context) + { + const double deliveryChance = 0.5; + if (_random.NextDouble() > deliveryChance) + { + throw new RpcException(new Status(StatusCode.Unavailable, $"- {request.Name}")); + } + + return Task.FromResult(new Response + { + Message = $"+ {request.Name}" + }); + } + } +} diff --git a/examples/Retrier/Server/Startup.cs b/examples/Retrier/Server/Startup.cs new file mode 100644 index 000000000..739f2fbf0 --- /dev/null +++ b/examples/Retrier/Server/Startup.cs @@ -0,0 +1,48 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC 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. + +#endregion + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Server +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddGrpc(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + }); + } + } +} diff --git a/examples/Retrier/Server/appsettings.Development.json b/examples/Retrier/Server/appsettings.Development.json new file mode 100644 index 000000000..fe20c40cc --- /dev/null +++ b/examples/Retrier/Server/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Grpc": "Information", + "Microsoft": "Information" + } + } +} diff --git a/examples/Retrier/Server/appsettings.json b/examples/Retrier/Server/appsettings.json new file mode 100644 index 000000000..f5f63744b --- /dev/null +++ b/examples/Retrier/Server/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" + } + } +}