Elsa V3學習之Flowchart詳解(上)

饭勺oO發表於2024-08-19

前面我們透過介面學習了Elsa的一些基本使用,若是有實操的小夥伴們,應該可以發現,我們工作流定義中的root,既我們的工作流畫布其實也是一個activity,就是Flowchart。那麼本文將來解讀以下flowchart的執行邏輯。

Flowchart原始碼

為了方便大家,這裡先直接把flowchart的原始碼貼出。

using System.ComponentModel;
using System.Runtime.CompilerServices;
using Elsa.Extensions;
using Elsa.Workflows.Activities.Flowchart.Contracts;
using Elsa.Workflows.Activities.Flowchart.Extensions;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Options;
using Elsa.Workflows.Signals;
using Microsoft.Extensions.Logging;

namespace Elsa.Workflows.Activities.Flowchart.Activities;

/// <summary>
/// A flowchart consists of a collection of activities and connections between them.
/// </summary>
[Activity("Elsa", "Flow", "A flowchart is a collection of activities and connections between them.")]
[Browsable(false)]
public class Flowchart : Container
{
    internal const string ScopeProperty = "Scope";

    /// <inheritdoc />
    public Flowchart([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
    {
        OnSignalReceived<ScheduleActivityOutcomes>(OnScheduleOutcomesAsync);
        OnSignalReceived<ScheduleChildActivity>(OnScheduleChildActivityAsync);
        OnSignalReceived<CancelSignal>(OnActivityCanceledAsync);
    }

    /// <summary>
    /// The activity to execute when the flowchart starts.
    /// </summary>
    [Port]
    [Browsable(false)]
    public IActivity? Start { get; set; }

    /// <summary>
    /// A list of connections between activities.
    /// </summary>
    public ICollection<Connection> Connections { get; set; } = new List<Connection>();

    /// <inheritdoc />
    protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContext context)
    {
        var startActivity = GetStartActivity(context);

        if (startActivity == null)
        {
            // Nothing else to execute.
            await context.CompleteActivityAsync();
            return;
        }

        // Schedule the start activity.
        await context.ScheduleActivityAsync(startActivity, OnChildCompletedAsync);
    }

    private IActivity? GetStartActivity(ActivityExecutionContext context)
    {
        // If there's a trigger that triggered this workflow, use that.
        var triggerActivityId = context.WorkflowExecutionContext.TriggerActivityId;
        var triggerActivity = triggerActivityId != null ? Activities.FirstOrDefault(x => x.Id == triggerActivityId) : default;

        if (triggerActivity != null)
            return triggerActivity;

        // If an explicit Start activity was provided, use that.
        if (Start != null)
            return Start;

        // If there is a Start activity on the flowchart, use that.
        var startActivity = Activities.FirstOrDefault(x => x is Start);

        if (startActivity != null)
            return startActivity;

        // If there's an activity marked as "Can Start Workflow", use that.
        var canStartWorkflowActivity = Activities.FirstOrDefault(x => x.GetCanStartWorkflow());

        if (canStartWorkflowActivity != null)
            return canStartWorkflowActivity;

        // If there is a single activity that has no inbound connections, use that.
        var root = GetRootActivity();

        if (root != null)
            return root;

        // If no start activity found, return the first activity.
        return Activities.FirstOrDefault();
    }

    /// <summary>
    /// Checks if there is any pending work for the flowchart.
    /// </summary>
    private bool HasPendingWork(ActivityExecutionContext context)
    {
        var workflowExecutionContext = context.WorkflowExecutionContext;
        var activityIds = Activities.Select(x => x.Id).ToList();
        var descendantContexts = context.GetDescendents().Where(x => x.ParentActivityExecutionContext == context).ToList();
        var activityExecutionContexts = descendantContexts.Where(x => activityIds.Contains(x.Activity.Id)).ToList();

        var hasPendingWork = workflowExecutionContext.Scheduler.List().Any(workItem =>
        {
            var ownerInstanceId = workItem.Owner?.Id;

            if (ownerInstanceId == null)
                return false;

            if (ownerInstanceId == context.Id)
                return true;

            var ownerContext = context.WorkflowExecutionContext.ActivityExecutionContexts.First(x => x.Id == ownerInstanceId);
            var ancestors = ownerContext.GetAncestors().ToList();

            return ancestors.Any(x => x == context);
        });

        var hasRunningActivityInstances = activityExecutionContexts.Any(x => x.Status == ActivityStatus.Running);

        return hasRunningActivityInstances || hasPendingWork;
    }

    private IActivity? GetRootActivity()
    {
        // Get the first activity that has no inbound connections.
        var query =
            from activity in Activities
            let inboundConnections = Connections.Any(x => x.Target.Activity == activity)
            where !inboundConnections
            select activity;

        var rootActivity = query.FirstOrDefault();
        return rootActivity;
    }

    private async ValueTask OnChildCompletedAsync(ActivityCompletedContext context)
    {
        var logger = context.GetRequiredService<ILogger<Flowchart>>();
        var flowchartContext = context.TargetContext;
        var completedActivityContext = context.ChildContext;
        var completedActivity = completedActivityContext.Activity;
        var result = context.Result;

        // If the complete activity's status is anything but "Completed", do not schedule its outbound activities.
        var scheduleChildren = completedActivityContext.Status == ActivityStatus.Completed;
        var outcomeNames = result is Outcomes outcomes
            ? outcomes.Names
            : [null!, "Done"];

        // Only query the outbound connections if the completed activity wasn't already completed.
        var outboundConnections = Connections.Where(connection => connection.Source.Activity == completedActivity && outcomeNames.Contains(connection.Source.Port)).ToList();
        var children = outboundConnections.Select(x => x.Target.Activity).ToList();
        var scope = flowchartContext.GetProperty(ScopeProperty, () => new FlowScope());

        scope.RegisterActivityExecution(completedActivity);

        // If the complete activity is a terminal node, complete the flowchart immediately.
        if (completedActivity is ITerminalNode)
        {
            await flowchartContext.CompleteActivityAsync();
        }
        else if (scheduleChildren)
        {
            if (children.Any())
            {
                // Schedule each child, but only if all of its left inbound activities have already executed.
                foreach (var activity in children)
                {
                    var existingActivity = scope.ContainsActivity(activity);
                    scope.AddActivity(activity);

                    var inboundActivities = Connections.LeftInboundActivities(activity).ToList();

                    // If the completed activity is not part of the left inbound path, always allow its children to be scheduled.
                    if (!inboundActivities.Contains(completedActivity))
                    {
                        await flowchartContext.ScheduleActivityAsync(activity, OnChildCompletedAsync);
                        continue;
                    }

                    // If the activity is anything but a join activity, only schedule it if all of its left-inbound activities have executed, effectively implementing a "wait all" join. 
                    if (activity is not IJoinNode)
                    {
                        var executionCount = scope.GetExecutionCount(activity);
                        var haveInboundActivitiesExecuted = inboundActivities.All(x => scope.GetExecutionCount(x) > executionCount);

                        if (haveInboundActivitiesExecuted) 
                            await flowchartContext.ScheduleActivityAsync(activity, OnChildCompletedAsync);
                    }
                    else
                    {
                        // Select an existing activity execution context for this activity, if any.
                        var joinContext = flowchartContext.WorkflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x =>
                            x.ParentActivityExecutionContext == flowchartContext && x.Activity == activity);
                        var scheduleWorkOptions = new ScheduleWorkOptions
                        {
                            CompletionCallback = OnChildCompletedAsync,
                            ExistingActivityExecutionContext = joinContext,
                            PreventDuplicateScheduling = true
                        };

                        if (joinContext != null)
                            logger.LogDebug("Next activity {ChildActivityId} is a join activity. Attaching to existing join context {JoinContext}", activity.Id, joinContext.Id);
                        else if (!existingActivity)
                            logger.LogDebug("Next activity {ChildActivityId} is a join activity. Creating new join context", activity.Id);
                        else
                        {
                            logger.LogDebug("Next activity {ChildActivityId} is a join activity. Join context was not found, but activity is already being created", activity.Id);
                            continue;
                        }

                        await flowchartContext.ScheduleActivityAsync(activity, scheduleWorkOptions);
                    }
                }
            }

            if (!children.Any())
            {
                await CompleteIfNoPendingWorkAsync(flowchartContext);
            }
        }

        flowchartContext.SetProperty(ScopeProperty, scope);
    }

    private async Task CompleteIfNoPendingWorkAsync(ActivityExecutionContext context)
    {
        var hasPendingWork = HasPendingWork(context);

        if (!hasPendingWork)
        {
            var hasFaultedActivities = context.GetActiveChildren().Any(x => x.Status == ActivityStatus.Faulted);

            if (!hasFaultedActivities)
            {
                await context.CompleteActivityAsync();
            }
        }
    }

    private async ValueTask OnScheduleOutcomesAsync(ScheduleActivityOutcomes signal, SignalContext context)
    {
        var flowchartContext = context.ReceiverActivityExecutionContext;
        var schedulingActivityContext = context.SenderActivityExecutionContext;
        var schedulingActivity = schedulingActivityContext.Activity;
        var outcomes = signal.Outcomes;
        var outboundConnections = Connections.Where(connection => connection.Source.Activity == schedulingActivity && outcomes.Contains(connection.Source.Port!)).ToList();
        var outboundActivities = outboundConnections.Select(x => x.Target.Activity).ToList();

        if (outboundActivities.Any())
        {
            // Schedule each child.
            foreach (var activity in outboundActivities) await flowchartContext.ScheduleActivityAsync(activity, OnChildCompletedAsync);
        }
    }

    private async ValueTask OnScheduleChildActivityAsync(ScheduleChildActivity signal, SignalContext context)
    {
        var flowchartContext = context.ReceiverActivityExecutionContext;
        var activity = signal.Activity;
        var activityExecutionContext = signal.ActivityExecutionContext;

        if (activityExecutionContext != null)
        {
            await flowchartContext.ScheduleActivityAsync(activityExecutionContext.Activity, new ScheduleWorkOptions
            {
                ExistingActivityExecutionContext = activityExecutionContext,
                CompletionCallback = OnChildCompletedAsync,
                Input = signal.Input
            });
        }
        else
        {
            await flowchartContext.ScheduleActivityAsync(activity, new ScheduleWorkOptions
            {
                CompletionCallback = OnChildCompletedAsync,
                Input = signal.Input
            });
        }
    }

    private async ValueTask OnActivityCanceledAsync(CancelSignal signal, SignalContext context)
    {
        await CompleteIfNoPendingWorkAsync(context.ReceiverActivityExecutionContext);
    }
}

首先我們從Activity特性中的描述引數中可以看到介紹flowchart作用的一句話:A flowchart is a collection of activities and connections between them.顯而易見,flowchart是一個儲存了多個Activity和他們連線關係的集合。有了這些資料,flowchart就可以根據connections中的連線關係對activity按照順序執行了。

Container

接下來我們再往下看,可以看到flowchart不是直接繼承Activity的基類,而是繼承Container。
Container包含了Activities和Variables兩個集合屬性,分別用於儲存我們的節點集合和變數集合。
在Container的執行入口方法中,先對變數進行了初始化和註冊。

protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
    // Ensure variables have names.
    EnsureNames(Variables);

    // Register variables.
    context.ExpressionExecutionContext.Memory.Declare(Variables);

    // Schedule children.
    await ScheduleChildrenAsync(context);
}

在最後呼叫了一個ScheduleChildrenAsync方法。這裡可以看到這個方法是一個虛方法,可以給子類重寫。

protected virtual ValueTask ScheduleChildrenAsync(ActivityExecutionContext context)
{
    ScheduleChildren(context);
    return ValueTask.CompletedTask;
}

在flowchart中,執行的入口正是這個重寫的ScheduleChildrenAsync方法。

Flowchart執行邏輯

迴歸正題,接下來我們繼續看Flowchart的入口,既ScheduleChildrenAsync方法。

protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContext context)
{
    var startActivity = GetStartActivity(context);

    if (startActivity == null)
    {
        // Nothing else to execute.
        await context.CompleteActivityAsync();
        return;
    }

    // Schedule the start activity.
    await context.ScheduleActivityAsync(startActivity, OnChildCompletedAsync);
}

先簡單過一下這幾行的邏輯,首先獲取StartActivity,既獲取第一個執行的工作流節點,如果獲取不到,這結束工作流。
如果獲取到了,那麼將發起排程,同時傳入一個回撥函式,這個回撥函式是工作流按照順序執行的關鍵。

GetStartActivity

那麼接下來看它是如何拿到起始節點的呢。

private IActivity? GetStartActivity(ActivityExecutionContext context)
{
    // If there's a trigger that triggered this workflow, use that.
    var triggerActivityId = context.WorkflowExecutionContext.TriggerActivityId;
    var triggerActivity = triggerActivityId != null ? Activities.FirstOrDefault(x => x.Id == triggerActivityId) : default;

    if (triggerActivity != null)
        return triggerActivity;

    // If an explicit Start activity was provided, use that.
    if (Start != null)
        return Start;

    // If there is a Start activity on the flowchart, use that.
    var startActivity = Activities.FirstOrDefault(x => x is Start);

    if (startActivity != null)
        return startActivity;

    // If there's an activity marked as "Can Start Workflow", use that.
    var canStartWorkflowActivity = Activities.FirstOrDefault(x => x.GetCanStartWorkflow());

    if (canStartWorkflowActivity != null)
        return canStartWorkflowActivity;

    // If there is a single activity that has no inbound connections, use that.
    var root = GetRootActivity();

    if (root != null)
        return root;

    // If no start activity found, return the first activity.
    return Activities.FirstOrDefault();
}

這裡從開頭可以看到,優先順序最高的StartActivity竟然不是Star,而是先獲取TriggerActivity,那麼什麼是TriggerActivity呢,就比如我們的HTTP Endpoint, Event, Cron這些,當我們拖到畫布當中時,預設會勾選Trigger workflow這個選項,如下圖中間最下方所示。至於他的觸發原理後續再深入探討,這裡就稍微過一下就好了。
image.png
若是沒有TriggerActivity,那麼flowchart會判斷Start屬性是否存在,如果存在表示明確指定了Start節點,那這個節點將作為工作流的起始節點。
若是Start也不存在,則會從所有的Activities中查詢第一個Start節點,若存在,則作為工作流起始節點。
若在Activities中也沒有Start節點,則再判斷一下是否有節點勾選了Start Of Workflow選項,若是勾選了,則獲取第一個勾選的Activity作為起始節點。
image.png
若是再沒有符合條件的節點,則會嘗試獲取root節點。

 private IActivity? GetRootActivity()
 {
     // Get the first activity that has no inbound connections.
     var query =
         from activity in Activities
         let inboundConnections = Connections.Any(x => x.Target.Activity == activity)
         where !inboundConnections
         select activity;

     var rootActivity = query.FirstOrDefault();
     return rootActivity;
 }

透過程式碼我們可以看到,root節點就是Connections連線關係中的第一個節點。
若是一堆節點裡面沒有任何連線關係,那麼最後則會在所有的Activity中取第一個當作入口。

可以看到,獲取我們的StartActivity的邏輯還是挺嚴謹的。

context.ScheduleActivityAsync

好了,獲取到了StartActivity之後,接下來就是真正的發起排程了,context.ScheduleActivityAsync方法就是把我們的StartActivity塞進去排程佇列,然後會自動執行節點。這執行的邏輯在後面的文章再解析。這個方法關鍵的是後面那個Callback方法。既OnChildCompletedAsync。

由於OnChildCompletedAsync的邏輯比較複雜,我們放到下一篇文章再繼續講解。

相關文章