Merge pull request #6447 from elsa-workflows/enh/send-http-request-activity-resilience
Add Resiliency to HTTP Request Activity
This commit is contained in:
commit
7145eae294
|
|
@ -112,34 +112,6 @@
|
|||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.SqlServer" Version="0.5.1" />
|
||||
<PackageVersion Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0'">
|
||||
<PackageVersion Include="AspNetCore.Authentication.ApiKey" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="7.0.20" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
|
||||
<PackageVersion Include="Npgsql" Version="7.0.7" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.18" />
|
||||
<PackageVersion Include="Polly" Version="7.2.4" />
|
||||
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageVersion Include="AspNetCore.Authentication.ApiKey" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.10" />
|
||||
|
|
@ -168,5 +140,7 @@
|
|||
<PackageVersion Include="Polly" Version="8.4.2" />
|
||||
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="8.10.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Resilience" Version="8.10.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
|
||||
|
||||
<PropertyGroup Label="TargetFrameworks">
|
||||
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Label="Files">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,4 @@
|
|||
<PackageReference Include="LinqKit.Core" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@
|
|||
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="Npgsql" VersionOverride="7.0.7" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="Npgsql" VersionOverride="8.0.3" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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<HttpResponseMessage>
|
|||
/// <summary>
|
||||
/// The URL to send the request to.
|
||||
/// </summary>
|
||||
[Input]
|
||||
public Input<Uri?> Url { get; set; } = default!;
|
||||
[Input] public Input<Uri?> Url { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP method to use when sending the request.
|
||||
|
|
@ -81,6 +82,11 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
)]
|
||||
public Input<HttpHeaders?> RequestHeaders { get; set; } = new(new HttpHeaders());
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether resiliency mechanisms should be enabled for the HTTP request.
|
||||
/// </summary>
|
||||
public Input<bool> EnableResiliency { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP response status code
|
||||
/// </summary>
|
||||
|
|
@ -122,15 +128,15 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
|
||||
private async Task TrySendAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var request = PrepareRequest(context);
|
||||
var logger = (ILogger)context.GetRequiredService(typeof(ILogger<>).MakeGenericType(GetType()));
|
||||
var httpClientFactory = context.GetRequiredService<IHttpClientFactory>();
|
||||
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 +153,7 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
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 +163,30 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
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<HttpResponseMessage> SendRequestAsync()
|
||||
{
|
||||
if (resiliencyEnabled)
|
||||
{
|
||||
var pipeline = BuildResiliencyPipeline(context);
|
||||
return await pipeline.ExecuteAsync(async ct => await SendRequestAsyncCore(ct), cancellationToken);
|
||||
}
|
||||
|
||||
return await SendRequestAsyncCore();
|
||||
}
|
||||
|
||||
async Task<HttpResponseMessage> SendRequestAsyncCore(CancellationToken ct = default)
|
||||
{
|
||||
var request = PrepareRequest(context);
|
||||
return await httpClient.SendAsync(request, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<object?> ParseContentAsync(ActivityExecutionContext context, HttpResponseMessage httpResponse)
|
||||
|
|
@ -195,7 +220,7 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
{
|
||||
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);
|
||||
|
|
@ -230,4 +255,45 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
|
|||
var parsedContentType = new System.Net.Mime.ContentType(contentType);
|
||||
return factories.FirstOrDefault(httpContentFactory => httpContentFactory.SupportedContentTypes.Any(c => c == parsedContentType.MediaType)) ?? new JsonContentFactory();
|
||||
}
|
||||
|
||||
private ResiliencePipeline<HttpResponseMessage> BuildResiliencyPipeline(ActivityExecutionContext context)
|
||||
{
|
||||
var pipelineBuilder = new ResiliencePipelineBuilder<HttpResponseMessage>()
|
||||
.AddRetry(new()
|
||||
{
|
||||
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
|
||||
.Handle<TimeoutException>() // Specific timeout exception
|
||||
.Handle<HttpRequestException>(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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -8,20 +8,22 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentStorage" />
|
||||
<PackageReference Include="FluentStorage"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Resilience"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<!--Overridden for vulnaribility reasons with dependencies referencing older versions.-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
|
||||
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.Liquid\Elsa.Liquid.csproj" />
|
||||
<ProjectReference Include="..\Elsa.SasTokens\Elsa.SasTokens.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
|
||||
<ProjectReference Include="..\Elsa.JavaScript\Elsa.JavaScript.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Liquid\Elsa.Liquid.csproj"/>
|
||||
<ProjectReference Include="..\Elsa.SasTokens\Elsa.SasTokens.csproj"/>
|
||||
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj"/>
|
||||
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj"/>
|
||||
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj"/>
|
||||
<ProjectReference Include="..\Elsa.JavaScript\Elsa.JavaScript.csproj"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@
|
|||
</ItemGroup>
|
||||
|
||||
<!--Overridden for vulnaribility reasons with dependencies referencing older versions.-->
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" VersionOverride="7.0.18" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" VersionOverride="8.0.4" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Description>
|
||||
Provides integration with the Quartz.NET library and provide am implementation of Elsa's IJobScheduler using Quartz.NET.
|
||||
</Description>
|
||||
|
|
|
|||
Loading…
Reference in a new issue