8000 IResilienceStrategy Abstraction with Category-Based Matching · Issue #6444 · elsa-workflows/elsa-core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

IResilienceStrategy Abstraction with Category-Based Matching #6444

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
sfmskywalker opened this issue Feb 24, 2025 · 0 comments
Open

IResilienceStrategy Abstraction with Category-Based Matching #6444

sfmskywalker opened this issue Feb 24, 2025 · 0 comments
Assignees
Milestone

Comments

@sfmskywalker
Copy link
Member
sfmskywalker commented Feb 24, 2025

Implement resilience capabilities by introducing a general-purpose IResilienceStrategy abstraction. This abstraction applies to various types of activities (e.g., HTTP requests, email sending, database operations) and allows users to select an appropriate resilience strategy based on activity context through category matching.

Proposed Approach

  • IResilienceStrategy Abstraction

    • Define a common IResilienceStrategy interface that all resilience strategies implement.
    • Ensure that all strategy classes are serializable so that configuration objects (from appsettings.json, a database, etc.) can be deserialized into concrete strategy instances.
    • Expose public properties on each strategy to allow user configuration. For example, the HTTP resilience strategy could expose the number of retries and a backoff factor (if it’s a retry-type policy).
  • Category-Based Matching with Attributes

    • Introduce a [ResilienceCategory("CategoryName")] attribute to annotate both strategy implementations and activities.
    • Example:
      • An HTTP-specific resilience strategy implementation would be annotated with [ResilienceCategory("HTTP")].
      • The SendHttpRequest activity would include a property (e.g., ResilienceStrategyId) annotated with [ResilienceCategory("HTTP")] to ensure that the UI filters and presents only strategies matching the HTTP category.
    • This mechanism allows the system to dynamically match activities with applicable resilience strategies based on shared tags.
  • Data Modeling & Configuration

    • Provide a configuration model (e.g., in appsettings.json or a database) where users can define a list of available strategy definitions.
    • A sample snippet of appsettings.json could define two configurations of the HTTP resilience strategy—one with 3 retry attempts and another with 10:
      {
        "ResilienceStrategies": [
          {
            "Type": "HttpResilienceStrategy",
            "Id": "HttpResilienceStrategy3",
            "RetryCount": 3,
            "BackoffFactor": 2.0
          },
          {
            "Type": "HttpResilienceStrategy",
            "Id": "HttpResilienceStrategy10",
            "RetryCount": 10,
            "BackoffFactor": 2.0
          }
        ]
      }
  • Usage in Activities

    • Each activity (e.g., SendHttpRequest and SendEmail) will include a ResilienceStrategyId property annotated with the relevant category attribute.
    • At runtime, using the activity execution context, the selected strategy is retrieved via a strategy service, deserialized from configuration, and then used to wrap the core activity logic.
    • When no strategy is selected, no resilience strategy is applied.
    • Only one resilience strategy can be selected per activity.

C# Pseudo Code Examples

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Polly;
using Polly.Retry;

// IResilienceStrategy Interface
public interface IResilienceStrategy
{
    string Id { get; }
    Task<T> ExecuteAsync<T>(Func<Task<T>> action);
}

// ResilienceCategory attribute for tagging strategies and activity properties
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false)]
public class ResilienceCategoryAttribute : Attribute
{
    public string Category { get; }
    public ResilienceCategoryAttribute(string category) => Category = category;
}

// HTTP Resilience Strategy Implementation using Polly
[ResilienceCategory("HTTP")]
public class HttpResilienceStrategy : IResilienceStrategy
{
    public string Id { get; set; } = "HttpResilienceStrategy";

    // Public properties for configuration
    public int RetryCount { get; set; } = 3;
    public double BackoffFactor { get; set; } = 2.0;

    public async Task<T> ExecuteAsync<T>(Func<Task<T>> action)
    {
        AsyncRetryPolicy<T> policy = Policy
            .Handle<Exception>()
            .WaitAndRetryAsync(
                RetryCount,
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(BackoffFactor, retryAttempt))
            );

        return await policy.ExecuteAsync(action);
    }
}

// Email Resilience Strategy Implementation using Polly
[ResilienceCategory("Email")]
public class EmailResilienceStrategy : IResilienceStrategy
{
    public string Id { get; set; } = "EmailResilienceStrategy";

    // Public properties for configuration
    public int RetryCount { get; set; } = 5;
    public int DelaySeconds { get; set; } = 1;

    public async Task<T> ExecuteAsync<T>(Func<Task<T>> action)
    {
        AsyncRetryPolicy<T> policy = Policy
            .Handle<Exception>()
            .WaitAndRetryAsync(
                RetryCount,
                retryAttempt => TimeSpan.FromSeconds(DelaySeconds * retryAttempt)
            );

        return await policy.ExecuteAsync(action);
    }
}

// A simple service to retrieve strategies by their ID
public interface IResilienceStrategyService
{
    IResilienceStrategy GetStrategyById(string id);
}

// Simplified Activity Execution Context for service resolution
public class ActivityExecutionContext
{
    private readonly IServiceProvider _serviceProvider;
    public ActivityExecutionContext(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
    public T GetService<T>() => (T)_serviceProvider.GetService(typeof(T));
}

// Simplified SendHttpRequest activity
public class SendHttpRequestActivity
{
    [ResilienceCategory("HTTP")]
    public string ResilienceStrategyId { get; set; }

    public async Task<HttpResponseMessage> ExecuteAsync(ActivityExecutionContext context)
    {
        var strategyService = context.GetService<IResilienceStrategyService>();
        IResilienceStrategy strategy = !string.IsNullOrEmpty(ResilienceStrategyId)
            ? strategyService.GetStrategyById(ResilienceStrategyId)
            : null;

        Func<Task<HttpResponseMessage>> action = async () =>
        {
            using (var httpClient = new HttpClient())
            {
                return await httpClient.GetAsync("https://example.com/api/resource");
            }
        };

        return strategy != null
            ? await strategy.ExecuteAsync(action)
            : await action();
    }
}

// Simplified SendEmail activity
public class SendEmailActivity
{
    [ResilienceCategory("Email")]
    public string ResilienceStrategyId { get; set; }

    public async Task ExecuteAsync(ActivityExecutionContext context)
    {
        var strategyService = context.GetService<IResilienceStrategyService>();
        IResilienceStrategy strategy = !string.IsNullOrEmpty(ResilienceStrategyId)
            ? strategyService.GetStrategyById(ResilienceStrategyId)
            : null;

        Func<Task<bool>> action = async () =>
        {
            await EmailService.SendEmailAsync("recipient@example.com", "Subject", "Body");
            return true;
        };

        if (strategy != null)
            await strategy.ExecuteAsync(action);
        else
            await action();
    }
}

// Dummy EmailService for demonstration purposes
public static class EmailService
{
    public static Task SendEmailAsync(string recipient, string subject, string body)
    {
        // Email sending implementation goes here
        return Task.CompletedTask;
    }
}

Conclusion
This feature will provide a flexible, extensible, and configuration-driven approach to applying resilience strategies across different types of activities in Elsa. By leveraging a common IResilienceStrategy interface, using category-based matching via attributes, and integrating Polly for proactive and reactive resilience policies, the system ensures that only appropriate resilience strategies are presented for each activity, thereby simplifying configuration and enhancing overall reliability.

@sfmskywalker sfmskywalker moved this to Triage in ELSA 3 Feb 24, 2025
@sfmskywalker sfmskywalker added this to the Elsa 3.4 milestone Feb 24, 2025
@sfmskywalker sfmskywalker changed the title General-Purpose IRetryStrategy Abstraction with Category-Based Matching IRetryStrategy Abstraction with Category-Based Matching Feb 24, 2025
@sfmskywalker sfmskywalker changed the title IRetryStrategy Abstraction with Category-Based Matching IResilienceStrategy Abstraction with Category-Based Matching Feb 24, 2025
@sfmskywalker sfmskywalker removed this from the Elsa 3.4 milestone Mar 17, 2025
@sfmskywalker sfmskywalker moved this from Triage to Todo in ELSA 3 May 7, 2025
@sfmskywalker sfmskywalker added this to the Elsa 3.5 milestone May 7, 2025
@sfmskywalker sfmskywalker self-assigned this May 7, 2025
@sfmskywalker sfmskywalker moved this from Todo to In Progress in ELSA 3 May 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress
Development

No branches or pull requests

1 participant
0