Add project files.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ResilienceCategoryAttribute(string category) : Attribute
|
||||
{
|
||||
public string Category { get; } = category;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Polly;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IResilienceStrategy
|
||||
{
|
||||
string Id { get; set; }
|
||||
string DisplayName { get; set; }
|
||||
Task ConfigurePipeline<T>(ResiliencePipelineBuilder<T> pipelineBuilder, ResilienceContext context);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IResilienceStrategyCatalog
|
||||
{
|
||||
Task<IEnumerable<IResilienceStrategy>> ListAsync(CancellationToken cancellationToken = default);
|
||||
Task<IResilienceStrategy?> GetAsync(string id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Resilience.Models;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IResilienceStrategyConfigEvaluator
|
||||
{
|
||||
Task<IResilienceStrategy?> EvaluateAsync(ResilienceStrategyConfig? config, ExpressionExecutionContext context, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IResilienceStrategySource
|
||||
{
|
||||
Task<IEnumerable<IResilienceStrategy>> GetStrategiesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Elsa.Resilience.Models;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IResilientActivity : IActivity
|
||||
{
|
||||
IDictionary<string, string?> CollectRetryDetails(ActivityExecutionContext context, RetryAttempt attempt);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to execute an activity's logic with a specified resilience strategy, or execute the action directly if no resilience configuration is applicable.
|
||||
/// </summary>
|
||||
public interface IResilientActivityInvoker
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes a resilient activity execution with the provided action using the activity's selected resilience strategy configuration.
|
||||
/// If no resilience configuration is provided or otherwise does not evaluate to a strategy, the action is called as-is.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result produced by the activity.</typeparam>
|
||||
/// <param name="activity">The resilient activity being invoked.</param>
|
||||
/// <param name="context">The execution context in which the activity runs.</param>
|
||||
/// <param name="action">The action representing the activity's execution logic.</param>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <returns>The result of the invoked action.</returns>
|
||||
Task<T> InvokeAsync<T>(IResilientActivity activity, ActivityExecutionContext context, Func<Task<T>> action, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Elsa.Common.Models;
|
||||
using Elsa.Resilience.Entities;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IRetryAttemptReader
|
||||
{
|
||||
Task<Page<RetryAttemptRecord>> ReadAttemptsAsync(string activityInstanceId, PageArgs? pageArgs = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Elsa.Resilience.Models;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IRetryAttemptRecorder
|
||||
{
|
||||
Task RecordAsync(RecordRetryAttemptsContext context);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides core services and types for implementing resilient services and activities.
|
||||
</Description>
|
||||
<PackageTags>elsa module resilience core</PackageTags>
|
||||
<RootNamespace>Elsa.Resilience</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Polly" />
|
||||
<PackageReference Include="Polly.Extensions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,4 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=attributes/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=contracts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Elsa.Common.Entities;
|
||||
|
||||
namespace Elsa.Resilience.Entities;
|
||||
|
||||
public class RetryAttemptRecord : Entity
|
||||
{
|
||||
public string ActivityInstanceId { get; set; } = null!;
|
||||
public string ActivityId { get; set; } = null!;
|
||||
public string WorkflowInstanceId { get; set; } = null!;
|
||||
public int AttemptNumber { get; set; }
|
||||
public TimeSpan RetryDelay { get; set; }
|
||||
public IDictionary<string, string> Details { get; set; } = new Dictionary<string, string>();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Resilience.Extensions;
|
||||
|
||||
public static class ActivityExecutionExtensions
|
||||
{
|
||||
private const string RetriesAttemptedFlag = "HasRetryAttempts";
|
||||
private const string ResilienceStrategy = "ResilienceStrategy";
|
||||
|
||||
public static void SetRetriesAttemptedFlag(this ActivityExecutionContext context)
|
||||
{
|
||||
var current = context;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
current.SetMetadata(RetriesAttemptedFlag, true);
|
||||
current = current.ParentActivityExecutionContext;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool GetRetriesAttemptedFlag(this ActivityExecutionContext context)
|
||||
{
|
||||
return context.GetMetadata(RetriesAttemptedFlag, () => false);
|
||||
}
|
||||
|
||||
public static void SetResilienceStrategy(this ActivityExecutionContext context, JsonNode model)
|
||||
{
|
||||
context.SetProperty(ResilienceStrategy, model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,6 @@
|
||||
using Elsa.Resilience.Entities;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Resilience.Models;
|
||||
|
||||
public record RecordRetryAttemptsContext(ActivityExecutionContext ActivityExecutionContext, ICollection<RetryAttemptRecord> Attempts, CancellationToken CancellationToken);
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Common.Serialization;
|
||||
using Elsa.Expressions.Models;
|
||||
|
||||
namespace Elsa.Resilience.Models;
|
||||
|
||||
public class ResilienceStrategyConfig
|
||||
{
|
||||
public ResilienceStrategyConfigMode Mode { get; set; }
|
||||
public string? StrategyId { get; set; }
|
||||
public Expression? Expression { get; set; }
|
||||
|
||||
public JsonNode SerializeToNode()
|
||||
{
|
||||
return JsonSerializer.SerializeToNode(this, SerializerOptions.ResilienceStrategyConfigSerializerOptions)!;
|
||||
}
|
||||
|
||||
public static ResilienceStrategyConfig? Deserialize(JsonNode? node)
|
||||
{
|
||||
return node.Deserialize<ResilienceStrategyConfig?>(SerializerOptions.ResilienceStrategyConfigSerializerOptions)!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Elsa.Resilience.Models;
|
||||
|
||||
public enum ResilienceStrategyConfigMode
|
||||
{
|
||||
Identifier,
|
||||
Expression,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Resilience.Models;
|
||||
|
||||
public record RetryAttempt(ActivityExecutionContext ActivityExecutionContext, int AttemptNumber, TimeSpan RetryDelay, object? Result, Exception? Exception)
|
||||
{
|
||||
internal static readonly object RetriesKey = new();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Elsa.Resilience.Options;
|
||||
|
||||
public class ResilienceOptions
|
||||
{
|
||||
public ICollection<Type> StrategyTypes { get; set; } = new List<Type>();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Resilience.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Resilience.Serialization;
|
||||
|
||||
public class ResilienceStrategySerializer
|
||||
{
|
||||
private readonly JsonSerializerOptions _serializerOptions;
|
||||
|
||||
public ResilienceStrategySerializer(IOptions<ResilienceOptions> options)
|
||||
{
|
||||
var serializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver().WithAddedModifier(typeInfo =>
|
||||
{
|
||||
if (typeInfo.Type != typeof(IResilienceStrategy))
|
||||
return;
|
||||
|
||||
if (typeInfo.Kind != JsonTypeInfoKind.Object)
|
||||
return;
|
||||
|
||||
var polymorphismOptions = new JsonPolymorphismOptions
|
||||
{
|
||||
TypeDiscriminatorPropertyName = "$type"
|
||||
};
|
||||
|
||||
foreach (var type in options.Value.StrategyTypes.ToList())
|
||||
polymorphismOptions.DerivedTypes.Add(new(type, type.Name));
|
||||
|
||||
typeInfo.PolymorphismOptions = polymorphismOptions;
|
||||
}),
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString
|
||||
}.WithConverters(new JsonStringEnumConverter());
|
||||
|
||||
_serializerOptions = serializerOptions;
|
||||
}
|
||||
|
||||
public JsonSerializerOptions SerializerOptions => _serializerOptions;
|
||||
public string Serialize(IResilienceStrategy strategy) => JsonSerializer.Serialize(strategy, _serializerOptions);
|
||||
public string SerializeMany(IEnumerable<IResilienceStrategy> strategies) => JsonSerializer.Serialize(strategies, _serializerOptions);
|
||||
public IResilienceStrategy Deserialize(string json) => JsonSerializer.Deserialize<IResilienceStrategy>(json, _serializerOptions)!;
|
||||
public IEnumerable<IResilienceStrategy> DeserializeMany(string json) => JsonSerializer.Deserialize<IEnumerable<IResilienceStrategy>>(json, _serializerOptions)!;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public class ResilienceStrategyCatalog : IResilienceStrategyCatalog
|
||||
{
|
||||
private readonly Lazy<Task<IEnumerable<IResilienceStrategy>>> _strategies;
|
||||
private readonly IEnumerable<IResilienceStrategySource> _providers;
|
||||
|
||||
public ResilienceStrategyCatalog(IEnumerable<IResilienceStrategySource> providers)
|
||||
{
|
||||
_providers = providers;
|
||||
_strategies = new(GetStrategiesInternalAsync, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<IResilienceStrategy>> ListAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _strategies.Value;
|
||||
}
|
||||
|
||||
public async Task<IResilienceStrategy?> GetAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var strategies = await ListAsync(cancellationToken);
|
||||
return strategies.FirstOrDefault(x => x.Id == id);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<IResilienceStrategy>> GetStrategiesInternalAsync()
|
||||
{
|
||||
var strategies = new List<IResilienceStrategy>();
|
||||
foreach (var provider in _providers) strategies.AddRange(await provider.GetStrategiesAsync());
|
||||
return strategies;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Dynamic;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Resilience.Models;
|
||||
using Elsa.Resilience.Serialization;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public class ResilienceStrategyConfigEvaluator(IResilienceStrategyCatalog catalog, IExpressionEvaluator expressionEvaluator, ResilienceStrategySerializer strategySerializer) : IResilienceStrategyConfigEvaluator
|
||||
{
|
||||
public async Task<IResilienceStrategy?> EvaluateAsync(ResilienceStrategyConfig? config, ExpressionExecutionContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (config == null) return null;
|
||||
|
||||
var mode = config.Mode;
|
||||
|
||||
return mode switch
|
||||
{
|
||||
ResilienceStrategyConfigMode.Identifier => await ResolveByIdentifierAsync(config, cancellationToken),
|
||||
ResilienceStrategyConfigMode.Expression => await ResolveByExpressionAsync(config, context, cancellationToken),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IResilienceStrategy?> ResolveByIdentifierAsync(ResilienceStrategyConfig config, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var strategyId = config.StrategyId;
|
||||
return string.IsNullOrWhiteSpace(strategyId)
|
||||
? null
|
||||
: await catalog.GetAsync(strategyId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<IResilienceStrategy?> ResolveByExpressionAsync(ResilienceStrategyConfig config, ExpressionExecutionContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var expression = config.Expression;
|
||||
|
||||
if (expression == null)
|
||||
return null;
|
||||
|
||||
var result = await expressionEvaluator.EvaluateAsync<object>(expression, context, ExpressionEvaluatorOptions.Empty);
|
||||
|
||||
return result switch
|
||||
{
|
||||
string strategyId => await catalog.GetAsync(strategyId, cancellationToken),
|
||||
IResilienceStrategy strategy => strategy,
|
||||
ExpandoObject expando => expando.ConvertTo<IResilienceStrategy>(new(strategySerializer.SerializerOptions)),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Text.Json;
|
||||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Resilience.Entities;
|
||||
using Elsa.Resilience.Extensions;
|
||||
using Elsa.Resilience.Models;
|
||||
using Elsa.Resilience.Serialization;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.State;
|
||||
using Polly;
|
||||
using Polly.Telemetry;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public class ResilientActivityInvoker(
|
||||
IResilienceStrategyConfigEvaluator resilienceStrategyConfigEvaluator,
|
||||
IRetryAttemptRecorder retryAttemptRecorder,
|
||||
IIdentityGenerator identityGenerator,
|
||||
ResilienceStrategySerializer resilienceStrategySerializer) : IResilientActivityInvoker
|
||||
{
|
||||
private const string ResilienceStrategyIdPropKey = "resilienceStrategy";
|
||||
|
||||
public async Task<T> InvokeAsync<T>(IResilientActivity activity, ActivityExecutionContext context, Func<Task<T>> action, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Get the resilience strategy.
|
||||
var strategyConfig = GetStrategyConfig(activity);
|
||||
var resilienceStrategy = await resilienceStrategyConfigEvaluator.EvaluateAsync(strategyConfig, context.ExpressionExecutionContext, cancellationToken);
|
||||
|
||||
// If no resilience strategy is configured, execute the action as-is.
|
||||
if (resilienceStrategy == null)
|
||||
return await action();
|
||||
|
||||
// Record the applied strategy as part of the activity execution context for diagnostics.
|
||||
var resilienceStrategyModel = JsonSerializer.SerializeToNode(resilienceStrategy, resilienceStrategySerializer.SerializerOptions)!;
|
||||
context.SetResilienceStrategy(resilienceStrategyModel);
|
||||
|
||||
// Create a resilience pipeline builder.
|
||||
var builder = CreateResiliencePipelineBuilder<T>();
|
||||
var retries = new List<RetryAttempt>();
|
||||
context.TransientProperties[RetryAttempt.RetriesKey] = retries;
|
||||
|
||||
// Create a resilience context.
|
||||
var resilienceContext = ResilienceContextPool.Shared.Get(cancellationToken);
|
||||
resilienceContext.Properties.Set(new(nameof(ActivityExecutionContext)), context);
|
||||
|
||||
try
|
||||
{
|
||||
// Configure the resilience pipeline.
|
||||
await resilienceStrategy.ConfigurePipeline(builder, resilienceContext);
|
||||
var pipeline = builder.Build();
|
||||
|
||||
// Execute the action within the resilience pipeline.
|
||||
var result = await pipeline.ExecuteAsync<T>(async _ => await action(), resilienceContext);
|
||||
|
||||
// Record the retry attempts.
|
||||
await RecordRetryAttempts(activity, context, retries, cancellationToken);
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ResilienceContextPool.Shared.Return(resilienceContext);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task RecordRetryAttempts(IResilientActivity activity, ActivityExecutionContext context, ICollection<RetryAttempt> attempts, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (attempts.Count > 0)
|
||||
{
|
||||
var records = Map(context, activity, attempts);
|
||||
var recordContext = new RecordRetryAttemptsContext(context, records, cancellationToken);
|
||||
await retryAttemptRecorder.RecordAsync(recordContext);
|
||||
|
||||
// Propagate a flag that retries have occurred. This information can then be used to show the retry attempts in the workflow designer.
|
||||
context.SetRetriesAttemptedFlag();
|
||||
}
|
||||
}
|
||||
|
||||
private ResiliencePipelineBuilder<T> CreateResiliencePipelineBuilder<T>()
|
||||
{
|
||||
var telemetryOptions = new TelemetryOptions();
|
||||
telemetryOptions.TelemetryListeners.Add(new RetryTelemetryListener());
|
||||
return new ResiliencePipelineBuilder<T>().ConfigureTelemetry(telemetryOptions);
|
||||
}
|
||||
|
||||
private ResilienceStrategyConfig? GetStrategyConfig(IResilientActivity resilientActivity)
|
||||
{
|
||||
return !resilientActivity.CustomProperties.TryGetValue(ResilienceStrategyIdPropKey, out var value)
|
||||
? null
|
||||
: value.ConvertTo<ResilienceStrategyConfig>();
|
||||
}
|
||||
|
||||
private ICollection<RetryAttemptRecord> Map(ActivityExecutionContext activityExecutionContext, IResilientActivity resilientActivity, ICollection<RetryAttempt> attempts)
|
||||
{
|
||||
return attempts.Select(x => Map(activityExecutionContext, resilientActivity, x)).ToList();
|
||||
}
|
||||
|
||||
private RetryAttemptRecord Map(ActivityExecutionContext activityExecutionContext, IResilientActivity resilientActivity, RetryAttempt attempt)
|
||||
{
|
||||
var details = resilientActivity.CollectRetryDetails(activityExecutionContext, attempt).Where(x => x.Value != null).ToDictionary(x => x.Key, x => x.Value!);
|
||||
return new()
|
||||
{
|
||||
Id = identityGenerator.GenerateId(),
|
||||
ActivityInstanceId = activityExecutionContext.Id,
|
||||
ActivityId = activityExecutionContext.Activity.Id,
|
||||
WorkflowInstanceId = activityExecutionContext.WorkflowExecutionContext.Id,
|
||||
AttemptNumber = attempt.AttemptNumber,
|
||||
RetryDelay = attempt.RetryDelay,
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Elsa.Resilience.Models;
|
||||
using Elsa.Workflows;
|
||||
using Polly.Retry;
|
||||
using Polly.Telemetry;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public class RetryTelemetryListener : TelemetryListener
|
||||
{
|
||||
public override void Write<TResult, TArgs>(in TelemetryEventArguments<TResult, TArgs> args)
|
||||
{
|
||||
if (args.Event.EventName != "OnRetry" || args.Arguments is not OnRetryArguments<TResult> retryArgs)
|
||||
return;
|
||||
|
||||
var ctx = args.Context;
|
||||
|
||||
if (!ctx.Properties.TryGetValue<ActivityExecutionContext>(new(nameof(ActivityExecutionContext)), out var activityExecutionContext))
|
||||
return;
|
||||
|
||||
var attempt = retryArgs.AttemptNumber;
|
||||
var delay = retryArgs.RetryDelay;
|
||||
var outcome = retryArgs.Outcome;
|
||||
var record = new RetryAttempt(activityExecutionContext, attempt, delay, outcome.Result, outcome.Exception);
|
||||
var records = (List<RetryAttempt>)activityExecutionContext.TransientProperties[RetryAttempt.RetriesKey];
|
||||
records.Add(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Elsa.Common.Models;
|
||||
using Elsa.Resilience.Entities;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public class VoidRetryAttemptReader : IRetryAttemptReader
|
||||
{
|
||||
public static VoidRetryAttemptReader Instance { get; } = new();
|
||||
|
||||
public Task<Page<RetryAttemptRecord>> ReadAttemptsAsync(string activityInstanceId, PageArgs? pageArgs = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(Page.Empty<RetryAttemptRecord>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Elsa.Resilience.Models;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public class VoidRetryAttemptRecorder : IRetryAttemptRecorder
|
||||
{
|
||||
public static VoidRetryAttemptRecorder Instance { get; } = new();
|
||||
|
||||
public Task RecordAsync(RecordRetryAttemptsContext context)
|
||||
{
|
||||
// Send records into the void.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user