From 484ecf3a88209b45e5b4002d60022e42edb22c88 Mon Sep 17 00:00:00 2001 From: Haik Date: Fri, 13 Sep 2024 13:44:56 +0400 Subject: [PATCH] nuget add --- .../AssemblyReference.cs | 3 + .../Pandatech.VerticalSlices.csproj | 13 ++- src/Pandatech.VerticalSlices/Program.cs | 12 +- .../Extensions/DatabaseExtensions.cs | 2 + .../Extensions/OpenTelemetryExtension.cs | 3 +- .../Extensions/ResilienceExtensions.cs | 33 ++++++ .../SharedKernel/Extensions/StartupLogger.cs | 4 +- .../ResilienceDefaultPipelineProvider.cs | 51 +++++++++ .../Helpers/ResilienceHttpOptions.cs | 103 ++++++++++++++++++ .../appsettings.Local.json | 3 +- .../Configurations/ApiFactory.cs | 4 +- .../Pandatech.VerticalSlices.Tests.csproj | 8 +- 12 files changed, 213 insertions(+), 26 deletions(-) create mode 100644 src/Pandatech.VerticalSlices/AssemblyReference.cs create mode 100644 src/Pandatech.VerticalSlices/SharedKernel/Extensions/ResilienceExtensions.cs create mode 100644 src/Pandatech.VerticalSlices/SharedKernel/Helpers/ResilienceDefaultPipelineProvider.cs create mode 100644 src/Pandatech.VerticalSlices/SharedKernel/Helpers/ResilienceHttpOptions.cs diff --git a/src/Pandatech.VerticalSlices/AssemblyReference.cs b/src/Pandatech.VerticalSlices/AssemblyReference.cs new file mode 100644 index 0000000..fd4e75e --- /dev/null +++ b/src/Pandatech.VerticalSlices/AssemblyReference.cs @@ -0,0 +1,3 @@ +namespace Pandatech.VerticalSlices; + +public record AssemblyReference(); \ No newline at end of file diff --git a/src/Pandatech.VerticalSlices/Pandatech.VerticalSlices.csproj b/src/Pandatech.VerticalSlices/Pandatech.VerticalSlices.csproj index 0cf3a9f..1d84698 100644 --- a/src/Pandatech.VerticalSlices/Pandatech.VerticalSlices.csproj +++ b/src/Pandatech.VerticalSlices/Pandatech.VerticalSlices.csproj @@ -1,13 +1,14 @@ - + - + + @@ -15,22 +16,22 @@ - + + - - + @@ -38,7 +39,7 @@ - + diff --git a/src/Pandatech.VerticalSlices/Program.cs b/src/Pandatech.VerticalSlices/Program.cs index 7dafe38..4e68c92 100644 --- a/src/Pandatech.VerticalSlices/Program.cs +++ b/src/Pandatech.VerticalSlices/Program.cs @@ -35,6 +35,7 @@ .ConfigureOpenTelemetry() .AddEndpoints() .AddGridify() + .AddResilienceDefaultPipeline() .AddCommunicator() .AddDistributedCache(options => { @@ -63,7 +64,7 @@ app.MapPandaEndpoints(); app.MapEndpoints(); -StartupLogger.LogStartSuccess(); +app.LogStartSuccess(); app.Run(); //todo Set appropriate name in github repo (ex. be-pt-pandatech-website). @@ -75,11 +76,4 @@ //todo Delete health checks and other configs of unrelated services. For example you might not need RMQ or Redis in this project. //todo Update all Nuget packages. //todo Include all required configurations in appsettings{environment}.json. -//todo Update ReadMm.md file. - -//Delete below rows if you have no integration Pandatech.VerticalSlices.Tests in your solution. - -namespace Pandatech.VerticalSlices -{ - public class Program; -} \ No newline at end of file +//todo Update ReadMm.md file. \ No newline at end of file diff --git a/src/Pandatech.VerticalSlices/SharedKernel/Extensions/DatabaseExtensions.cs b/src/Pandatech.VerticalSlices/SharedKernel/Extensions/DatabaseExtensions.cs index d45c3d9..9be48cd 100644 --- a/src/Pandatech.VerticalSlices/SharedKernel/Extensions/DatabaseExtensions.cs +++ b/src/Pandatech.VerticalSlices/SharedKernel/Extensions/DatabaseExtensions.cs @@ -1,4 +1,5 @@ using EFCore.PostgresExtensions.Extensions; +using EntityFramework.Exceptions.PostgreSQL; using Microsoft.EntityFrameworkCore; using Pandatech.VerticalSlices.Context; using Pandatech.VerticalSlices.SharedKernel.Helpers; @@ -15,6 +16,7 @@ public static WebApplicationBuilder AddPostgresContext(this WebApplicationBuilde builder.Services.AddDbContextPool(options => options.UseNpgsql(connectionString) .UseQueryLocks() + .UseExceptionProcessor() .UseSnakeCaseNamingConvention()); return builder; } diff --git a/src/Pandatech.VerticalSlices/SharedKernel/Extensions/OpenTelemetryExtension.cs b/src/Pandatech.VerticalSlices/SharedKernel/Extensions/OpenTelemetryExtension.cs index f4ebc3a..3bc14fd 100644 --- a/src/Pandatech.VerticalSlices/SharedKernel/Extensions/OpenTelemetryExtension.cs +++ b/src/Pandatech.VerticalSlices/SharedKernel/Extensions/OpenTelemetryExtension.cs @@ -27,8 +27,7 @@ public static WebApplicationBuilder ConfigureOpenTelemetry(this WebApplicationBu .WithTracing(tracing => { tracing.AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddGrpcClientInstrumentation(); + .AddHttpClientInstrumentation(); }); return builder; diff --git a/src/Pandatech.VerticalSlices/SharedKernel/Extensions/ResilienceExtensions.cs b/src/Pandatech.VerticalSlices/SharedKernel/Extensions/ResilienceExtensions.cs new file mode 100644 index 0000000..5b3331f --- /dev/null +++ b/src/Pandatech.VerticalSlices/SharedKernel/Extensions/ResilienceExtensions.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Http.Resilience; +using Pandatech.VerticalSlices.SharedKernel.Helpers; +using Polly; + +namespace Pandatech.VerticalSlices.SharedKernel.Extensions; + +public static class ResilienceExtensions +{ + public static WebApplicationBuilder AddResilienceDefaultPipeline(this WebApplicationBuilder builder) + { + builder.Services.AddResiliencePipeline(ResilienceDefaultPipelineProvider.DefaultPipelineName, + pipelineBuilder => + { + pipelineBuilder.AddRetry(ResilienceDefaultPipelineProvider.DefaultNetworkRetryOptions) + .AddRetry(ResilienceDefaultPipelineProvider.TooManyRequestsRetryOptions) + .AddCircuitBreaker(ResilienceDefaultPipelineProvider.DefaultCircuitBreakerOptions) + .AddTimeout(TimeSpan.FromSeconds(8)); + }); + return builder; + } + + public static IHttpResiliencePipelineBuilder AddResilienceDefaultPipeline(this IHttpClientBuilder builder) + { + return builder.AddResilienceHandler("DefaultPipeline", + resilienceBuilder => + { + resilienceBuilder.AddRetry(ResilienceHttpOptions.DefaultTooManyRequestsRetryOptions) + .AddRetry(ResilienceHttpOptions.DefaultNetworkRetryOptions) + .AddCircuitBreaker(ResilienceHttpOptions.DefaultCircuitBreakerOptions) + .AddTimeout(TimeSpan.FromSeconds(8)); + }); + } +} \ No newline at end of file diff --git a/src/Pandatech.VerticalSlices/SharedKernel/Extensions/StartupLogger.cs b/src/Pandatech.VerticalSlices/SharedKernel/Extensions/StartupLogger.cs index c3228f2..00f9021 100644 --- a/src/Pandatech.VerticalSlices/SharedKernel/Extensions/StartupLogger.cs +++ b/src/Pandatech.VerticalSlices/SharedKernel/Extensions/StartupLogger.cs @@ -22,7 +22,7 @@ public static WebApplicationBuilder LogStartAttempt(this WebApplicationBuilder b return builder; } - public static void LogStartSuccess() + public static WebApplication LogStartSuccess(this WebApplication app) { _stopwatch.Stop(); var now = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); @@ -33,5 +33,7 @@ public static void LogStartSuccess() Event = "ApplicationStartSuccess", InitializationTime = $"{initializationTime} seconds" })); + + return app; } } \ No newline at end of file diff --git a/src/Pandatech.VerticalSlices/SharedKernel/Helpers/ResilienceDefaultPipelineProvider.cs b/src/Pandatech.VerticalSlices/SharedKernel/Helpers/ResilienceDefaultPipelineProvider.cs new file mode 100644 index 0000000..c5c180a --- /dev/null +++ b/src/Pandatech.VerticalSlices/SharedKernel/Helpers/ResilienceDefaultPipelineProvider.cs @@ -0,0 +1,51 @@ +using System.Net; +using Polly; +using Polly.CircuitBreaker; +using Polly.Registry; +using Polly.Retry; + +namespace Pandatech.VerticalSlices.SharedKernel.Helpers; + +public static class ResilienceDefaultPipelineProvider +{ + public static ResiliencePipeline GetDefaultPipeline( + this ResiliencePipelineProvider resiliencePipelineProvider) + { + return resiliencePipelineProvider.GetPipeline(DefaultPipelineName); + } + + internal const string DefaultPipelineName = "DefaultPipeline"; + + internal static RetryStrategyOptions TooManyRequestsRetryOptions => + new() + { + MaxRetryAttempts = 5, + BackoffType = DelayBackoffType.Exponential, + UseJitter = true, + Delay = TimeSpan.FromMilliseconds(3000), + ShouldHandle = new PredicateBuilder() + .Handle(exception => exception.StatusCode == HttpStatusCode.TooManyRequests) + }; + + internal static RetryStrategyOptions DefaultNetworkRetryOptions => + new() + { + MaxRetryAttempts = 7, + BackoffType = DelayBackoffType.Exponential, + UseJitter = true, + Delay = TimeSpan.FromMilliseconds(800), + ShouldHandle = new PredicateBuilder() + .Handle(exception => exception.StatusCode == HttpStatusCode.RequestTimeout || + (int)exception.StatusCode! >= 500) + }; + + internal static CircuitBreakerStrategyOptions DefaultCircuitBreakerOptions => + new() + { + FailureRatio = 0.5, + SamplingDuration = TimeSpan.FromSeconds(30), + MinimumThroughput = 200, + BreakDuration = TimeSpan.FromSeconds(45), + ShouldHandle = new PredicateBuilder().Handle() + }; +} \ No newline at end of file diff --git a/src/Pandatech.VerticalSlices/SharedKernel/Helpers/ResilienceHttpOptions.cs b/src/Pandatech.VerticalSlices/SharedKernel/Helpers/ResilienceHttpOptions.cs new file mode 100644 index 0000000..7c54c3e --- /dev/null +++ b/src/Pandatech.VerticalSlices/SharedKernel/Helpers/ResilienceHttpOptions.cs @@ -0,0 +1,103 @@ +using System.Net; +using Microsoft.Extensions.Http.Resilience; +using Polly; + +namespace Pandatech.VerticalSlices.SharedKernel.Helpers; + +internal static class ResilienceHttpOptions +{ + public static HttpRetryStrategyOptions DefaultTooManyRequestsRetryOptions => + new() + { + MaxRetryAttempts = 5, + BackoffType = DelayBackoffType.Exponential, + UseJitter = true, + Delay = TimeSpan.FromMilliseconds(3000), + ShouldHandle = args => + { + if (args.Outcome.Exception is HttpRequestException httpException) + { + return ValueTask.FromResult((int)httpException.StatusCode! == 429); + } + + if (args.Outcome.Result is not null && args.Outcome.Result.StatusCode == HttpStatusCode.TooManyRequests) + { + return ValueTask.FromResult(true); + } + + return ValueTask.FromResult(false); + }, + DelayGenerator = args => + { + if (args.Outcome.Result is null) + { + return ValueTask.FromResult(null); + } + + if (!args.Outcome.Result.Headers.TryGetValues("Retry-After", out var values)) + { + return ValueTask.FromResult(null); + } + + var retryAfterValue = values.FirstOrDefault(); + + if (int.TryParse(retryAfterValue, out var retryAfterSeconds)) + { + return ValueTask.FromResult(TimeSpan.FromSeconds(retryAfterSeconds)); + } + + if (!DateTimeOffset.TryParseExact(retryAfterValue, + "R", // RFC1123 pattern + System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.None, + out var retryAfterDate)) + { + return ValueTask.FromResult(null); + } + + var retryDelay = retryAfterDate - DateTimeOffset.UtcNow; + return ValueTask.FromResult(retryDelay > TimeSpan.Zero ? retryDelay : TimeSpan.MinValue); + } + }; + + public static HttpRetryStrategyOptions DefaultNetworkRetryOptions => + new() + { + MaxRetryAttempts = 7, + BackoffType = DelayBackoffType.Exponential, + UseJitter = true, + Delay = TimeSpan.FromMilliseconds(800), + ShouldHandle = args => + { + if (args.Outcome.Exception is HttpRequestException httpException) + { + return ValueTask.FromResult((int)httpException.StatusCode! >= 500 || + (int)httpException.StatusCode! == 408); + } + + return ValueTask.FromResult(args.Outcome.Result is not null && + (args.Outcome.Result.StatusCode == HttpStatusCode.RequestTimeout || + (int)args.Outcome.Result.StatusCode >= 500)); + } + }; + + public static HttpCircuitBreakerStrategyOptions DefaultCircuitBreakerOptions => + new() + { + FailureRatio = 0.5, + SamplingDuration = TimeSpan.FromSeconds(30), + MinimumThroughput = 200, + BreakDuration = TimeSpan.FromSeconds(45), + ShouldHandle = args => + { + if (args.Outcome.Exception is not null) + { + return ValueTask.FromResult(true); + } + + return args.Outcome.Result is null + ? ValueTask.FromResult(false) + : ValueTask.FromResult(!args.Outcome.Result.IsSuccessStatusCode); + } + }; +} \ No newline at end of file diff --git a/src/Pandatech.VerticalSlices/appsettings.Local.json b/src/Pandatech.VerticalSlices/appsettings.Local.json index e45d778..cb6a606 100644 --- a/src/Pandatech.VerticalSlices/appsettings.Local.json +++ b/src/Pandatech.VerticalSlices/appsettings.Local.json @@ -20,7 +20,8 @@ "ConnectionStrings": { "Postgres": "Server=localhost;Port=5432;Database=pandatech_vertical_slices;User Id=test;Password=test;Pooling=true;", "Redis": "localhost:6379", - "RabbitMq": "amqp://test:test@localhost:5672" + "RabbitMq": "amqp://test:test@localhost:5672", + "PersistentStorage": "/persistence" }, "Security": { "SuperUser": { diff --git a/test/Pandatech.VerticalSlices.Tests/Configurations/ApiFactory.cs b/test/Pandatech.VerticalSlices.Tests/Configurations/ApiFactory.cs index f80e1e4..0a3acdd 100644 --- a/test/Pandatech.VerticalSlices.Tests/Configurations/ApiFactory.cs +++ b/test/Pandatech.VerticalSlices.Tests/Configurations/ApiFactory.cs @@ -12,7 +12,7 @@ namespace Pandatech.VerticalSlices.Tests.Configurations; -public class ApiFactory : WebApplicationFactory, IAsyncLifetime +public class ApiFactory : WebApplicationFactory, IAsyncLifetime { private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder() .Build(); @@ -88,8 +88,6 @@ private void SetEnvironments() Environment.SetEnvironmentVariable("RABBITMQ_ROUTING_KEY_DLX", "panda-dlx"); Environment.SetEnvironmentVariable("RABBITMQ_QUEUE_NAME_DLX", "panda-dlx"); Environment.SetEnvironmentVariable("RABBITMQ_URI", "amqp://guest:guest@localhost:5672"); - Environment.SetEnvironmentVariable("ELASTIC_SEARCH_URL", "http://localhost:9200"); - Environment.SetEnvironmentVariable("ELASTIC_INDEX_NAME", "panda"); Environment.SetEnvironmentVariable("CORS_ALLOWED_ORIGINS", "http://localhost:3000"); Environment.SetEnvironmentVariable("USER_MANAGEMENT_ADDRESS", "http://localhost:5000"); } diff --git a/test/Pandatech.VerticalSlices.Tests/Pandatech.VerticalSlices.Tests.csproj b/test/Pandatech.VerticalSlices.Tests/Pandatech.VerticalSlices.Tests.csproj index 98e51cb..e7dbc94 100644 --- a/test/Pandatech.VerticalSlices.Tests/Pandatech.VerticalSlices.Tests.csproj +++ b/test/Pandatech.VerticalSlices.Tests/Pandatech.VerticalSlices.Tests.csproj @@ -6,16 +6,16 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive