From a24ca0435fed8e76d4b7d7da740b4eecdf564df2 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 24 Feb 2025 22:29:51 +0100 Subject: [PATCH 1/3] Refactor projects to target .NET 8.0 exclusively. Dropped support for .NET 6.0 and .NET 7.0 by removing multi-targeting configurations. Updated dependencies to use versions compatible with .NET 8.0, ensuring consistency across all projects. This simplifies maintenance and aligns with the latest .NET standards. --- Directory.Packages.props | 30 ++----------------- ...Net.EntityFrameworkCore.PostgresSql.csproj | 2 +- src/Directory.Build.props | 2 +- .../Elsa.Server.LoadBalancer.csproj | 2 +- .../Elsa.Server.Web/Elsa.Server.Web.csproj | 2 +- .../Elsa.ServerAndStudio.Web.csproj | 2 +- .../Elsa.Studio.Web/Elsa.Studio.Web.csproj | 2 +- .../ElsaStudioWebAssembly.csproj | 2 +- src/modules/Elsa.Common/Elsa.Common.csproj | 4 --- src/modules/Elsa.Dapper/Elsa.Dapper.csproj | 3 -- ...artz.EntityFrameworkCore.PostgreSql.csproj | 3 -- src/modules/Elsa.Quartz/Elsa.Quartz.csproj | 2 +- 12 files changed, 10 insertions(+), 46 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fe346ae0e..8f97a65f2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -112,34 +112,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -168,5 +140,7 @@ + + \ No newline at end of file diff --git a/samples/aspnet/Elsa.Samples.AspNet.EntityFrameworkCore.PostgresSql/Elsa.Samples.AspNet.EntityFrameworkCore.PostgresSql.csproj b/samples/aspnet/Elsa.Samples.AspNet.EntityFrameworkCore.PostgresSql/Elsa.Samples.AspNet.EntityFrameworkCore.PostgresSql.csproj index b0593ffb9..ff765f2dc 100644 --- a/samples/aspnet/Elsa.Samples.AspNet.EntityFrameworkCore.PostgresSql/Elsa.Samples.AspNet.EntityFrameworkCore.PostgresSql.csproj +++ b/samples/aspnet/Elsa.Samples.AspNet.EntityFrameworkCore.PostgresSql/Elsa.Samples.AspNet.EntityFrameworkCore.PostgresSql.csproj @@ -1,7 +1,7 @@ - net7.0;net8.0 + net8.0 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 843d67641..ca5601874 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ - net6.0;net7.0;net8.0 + net8.0 diff --git a/src/bundles/Elsa.Server.LoadBalancer/Elsa.Server.LoadBalancer.csproj b/src/bundles/Elsa.Server.LoadBalancer/Elsa.Server.LoadBalancer.csproj index ab5408636..90dec8fea 100644 --- a/src/bundles/Elsa.Server.LoadBalancer/Elsa.Server.LoadBalancer.csproj +++ b/src/bundles/Elsa.Server.LoadBalancer/Elsa.Server.LoadBalancer.csproj @@ -1,7 +1,7 @@ - net7.0;net8.0 + net8.0 Linux false latest diff --git a/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj index 40bce5491..dd057dc90 100644 --- a/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -1,7 +1,7 @@ - net7.0;net8.0 + net8.0 Linux false latest diff --git a/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj b/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj index e2d1c97f8..5064e0750 100644 --- a/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj +++ b/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj @@ -1,7 +1,7 @@ - net7.0;net8.0 + net8.0 Linux false diff --git a/src/bundles/Elsa.Studio.Web/Elsa.Studio.Web.csproj b/src/bundles/Elsa.Studio.Web/Elsa.Studio.Web.csproj index 2cb27ad13..e425e69b4 100644 --- a/src/bundles/Elsa.Studio.Web/Elsa.Studio.Web.csproj +++ b/src/bundles/Elsa.Studio.Web/Elsa.Studio.Web.csproj @@ -1,7 +1,7 @@ - net7.0;net8.0 + net8.0 Linux false diff --git a/src/bundles/ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj b/src/bundles/ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj index eba2a2fd1..02c5db25a 100644 --- a/src/bundles/ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj +++ b/src/bundles/ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj @@ -1,7 +1,7 @@ - net7.0;net8.0 + net8.0 diff --git a/src/modules/Elsa.Common/Elsa.Common.csproj b/src/modules/Elsa.Common/Elsa.Common.csproj index aeee7011b..869fd520a 100644 --- a/src/modules/Elsa.Common/Elsa.Common.csproj +++ b/src/modules/Elsa.Common/Elsa.Common.csproj @@ -18,8 +18,4 @@ - - - - diff --git a/src/modules/Elsa.Dapper/Elsa.Dapper.csproj b/src/modules/Elsa.Dapper/Elsa.Dapper.csproj index dc9a4279f..5d4affe7a 100644 --- a/src/modules/Elsa.Dapper/Elsa.Dapper.csproj +++ b/src/modules/Elsa.Dapper/Elsa.Dapper.csproj @@ -14,9 +14,6 @@ - - - diff --git a/src/modules/Elsa.Quartz.EntityFrameworkCore.PostgreSql/Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj b/src/modules/Elsa.Quartz.EntityFrameworkCore.PostgreSql/Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj index 9b887cfcd..cfc79647c 100644 --- a/src/modules/Elsa.Quartz.EntityFrameworkCore.PostgreSql/Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj +++ b/src/modules/Elsa.Quartz.EntityFrameworkCore.PostgreSql/Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj @@ -15,9 +15,6 @@ - - - diff --git a/src/modules/Elsa.Quartz/Elsa.Quartz.csproj b/src/modules/Elsa.Quartz/Elsa.Quartz.csproj index 4498802c4..9776c86fc 100644 --- a/src/modules/Elsa.Quartz/Elsa.Quartz.csproj +++ b/src/modules/Elsa.Quartz/Elsa.Quartz.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net8.0 Provides integration with the Quartz.NET library and provide am implementation of Elsa's IJobScheduler using Quartz.NET. From 6ba50020fee919af340ca7c48520a3656e7c8f6c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 24 Feb 2025 22:30:56 +0100 Subject: [PATCH 2/3] Add HTTP resiliency support using Polly and pipeline builder Introduce configurable resiliency mechanisms for HTTP requests, including retries, circuit breakers, and timeouts, leveraging Microsoft.Extensions.Resilience and Polly. Refactor `SendHttpRequestBase` to include an `EnableResiliency` input and encapsulate resiliency logic in a dedicated pipeline. Update project references to include necessary dependencies. --- .../Activities/SendHttpRequestBase.cs | 83 +++++++++++++++++-- src/modules/Elsa.Http/Elsa.Http.csproj | 20 +++-- 2 files changed, 86 insertions(+), 17 deletions(-) diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs b/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs index 04085dbd8..3da8792b4 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Net.Http.Headers; using Elsa.Extensions; using Elsa.Http.ContentWriters; @@ -7,6 +8,7 @@ using Elsa.Workflows.Attributes; using Elsa.Workflows.UIHints; using Elsa.Workflows.Models; using Microsoft.Extensions.Logging; +using Polly; using HttpHeaders = Elsa.Http.Models.HttpHeaders; namespace Elsa.Http; @@ -25,8 +27,7 @@ public abstract class SendHttpRequestBase : Activity /// /// The URL to send the request to. /// - [Input] - public Input Url { get; set; } = default!; + [Input] public Input Url { get; set; } = default!; /// /// The HTTP method to use when sending the request. @@ -81,6 +82,11 @@ public abstract class SendHttpRequestBase : Activity )] public Input RequestHeaders { get; set; } = new(new HttpHeaders()); + /// + /// Indicates whether resiliency mechanisms should be enabled for the HTTP request. + /// + public Input EnableResiliency { get; set; } = default!; + /// /// The HTTP response status code /// @@ -122,15 +128,16 @@ public abstract class SendHttpRequestBase : Activity private async Task TrySendAsync(ActivityExecutionContext context) { - var request = PrepareRequest(context); + var logger = (ILogger)context.GetRequiredService(typeof(ILogger<>).MakeGenericType(GetType())); var httpClientFactory = context.GetRequiredService(); var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase)); var cancellationToken = context.CancellationToken; + var resiliencyEnabled = EnableResiliency.GetOrDefault(context, () => false); try { - var response = await httpClient.SendAsync(request, cancellationToken); + var response = await SendRequestAsync(); var parsedContent = await ParseContentAsync(context, response); var statusCode = (int)response.StatusCode; var responseHeaders = new HttpHeaders(response.Headers); @@ -147,7 +154,7 @@ public abstract class SendHttpRequestBase : Activity logger.LogWarning(e, "An error occurred while sending an HTTP request"); context.AddExecutionLogEntry("Error", e.Message, payload: new { - StackTrace = e.StackTrace + e.StackTrace }); context.JournalData.Add("Error", e.Message); await HandleRequestExceptionAsync(context, e); @@ -157,11 +164,30 @@ public abstract class SendHttpRequestBase : Activity logger.LogWarning(e, "An error occurred while sending an HTTP request"); context.AddExecutionLogEntry("Error", e.Message, payload: new { - StackTrace = e.StackTrace + e.StackTrace }); context.JournalData.Add("Cancelled", true); await HandleTaskCanceledExceptionAsync(context, e); } + + return; + + async Task SendRequestAsync() + { + if (resiliencyEnabled) + { + var pipeline = BuildResiliencyPipeline(context); + return await pipeline.ExecuteAsync(async ct => await SendRequestAsyncCore(ct), cancellationToken); + } + + return await SendRequestAsyncCore(); + } + + async Task SendRequestAsyncCore(CancellationToken ct = default) + { + var request = PrepareRequest(context); + return await httpClient.SendAsync(request, ct); + } } private async Task ParseContentAsync(ActivityExecutionContext context, HttpResponseMessage httpResponse) @@ -195,7 +221,7 @@ public abstract class SendHttpRequestBase : Activity { var method = Method.GetOrDefault(context) ?? "GET"; var url = Url.Get(context); - var request = new HttpRequestMessage(new HttpMethod(method), url); + var request = new HttpRequestMessage(new(method), url); var headers = context.GetHeaders(RequestHeaders); var authorization = Authorization.GetOrDefault(context); var addAuthorizationWithoutValidation = DisableAuthorizationHeaderValidation.GetOrDefault(context); @@ -218,7 +244,7 @@ public abstract class SendHttpRequestBase : Activity var factory = SelectContentWriter(contentType, factories); request.Content = factory.CreateHttpContent(content, contentType); } - + return request; } @@ -230,4 +256,45 @@ public abstract class SendHttpRequestBase : Activity var parsedContentType = new System.Net.Mime.ContentType(contentType); return factories.FirstOrDefault(httpContentFactory => httpContentFactory.SupportedContentTypes.Any(c => c == parsedContentType.MediaType)) ?? new JsonContentFactory(); } + + private ResiliencePipeline BuildResiliencyPipeline(ActivityExecutionContext context) + { + var pipelineBuilder = new ResiliencePipelineBuilder() + .AddRetry(new() + { + ShouldHandle = new PredicateBuilder() + .Handle() // Specific timeout exception + .Handle(ex => IsTransientStatusCode(ex.StatusCode)) // Network errors or transient HTTP codes + .HandleResult(response => IsTransientStatusCode(response.StatusCode)), + MaxRetryAttempts = 3, + Delay = TimeSpan.FromSeconds(Math.Min(Random.Shared.NextDouble() * 2, 8)), // Jittered delay capped at 8 secs + BackoffType = DelayBackoffType.Exponential + }) + .AddCircuitBreaker(new() + { + FailureRatio = 0.5, + SamplingDuration = TimeSpan.FromSeconds(30), + MinimumThroughput = 10, + BreakDuration = TimeSpan.FromSeconds(60) + }) + .AddTimeout(TimeSpan.FromSeconds(60)); // Outer timeout + + return pipelineBuilder.Build(); + } + + // Helper method to identify transient status codes. + private static bool IsTransientStatusCode(HttpStatusCode? statusCode) + { + if (!statusCode.HasValue) return true; // No status code (e.g., network failure) is worth retrying + return statusCode switch + { + HttpStatusCode.RequestTimeout => true, // 408 + HttpStatusCode.TooManyRequests => true, // 429 + HttpStatusCode.InternalServerError => true, // 500 + HttpStatusCode.BadGateway => true, // 502 + HttpStatusCode.ServiceUnavailable => true, // 503 + HttpStatusCode.GatewayTimeout => true, // 504 + _ => false + }; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Http/Elsa.Http.csproj b/src/modules/Elsa.Http/Elsa.Http.csproj index 26f35e9b5..caada764c 100644 --- a/src/modules/Elsa.Http/Elsa.Http.csproj +++ b/src/modules/Elsa.Http/Elsa.Http.csproj @@ -8,20 +8,22 @@ - + + + - + - + - - - - - - + + + + + + From f45388fbfc590d6c5f23f948bdf7bdd1455d1f4f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 24 Feb 2025 22:32:32 +0100 Subject: [PATCH 3/3] Remove unnecessary whitespace in SendHttpRequestBase.cs Cleaned up extra blank lines in the code to improve readability and maintain consistent formatting. These changes do not impact functionality or behavior of the code. --- src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs b/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs index 3da8792b4..a10e6ca19 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequestBase.cs @@ -128,7 +128,6 @@ public abstract class SendHttpRequestBase : Activity private async Task TrySendAsync(ActivityExecutionContext context) { - var logger = (ILogger)context.GetRequiredService(typeof(ILogger<>).MakeGenericType(GetType())); var httpClientFactory = context.GetRequiredService(); var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase)); @@ -244,7 +243,7 @@ public abstract class SendHttpRequestBase : Activity var factory = SelectContentWriter(contentType, factories); request.Content = factory.CreateHttpContent(content, contentType); } - + return request; }