前面我们通过界面学习了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这个选项,如下图中间最下方所示。至于他的触发原理后续再深入探讨,这里就稍微过一下就好了。



若是没有TriggerActivity,那么flowchart会判断Start属性是否存在,如果存在表示明确指定了Start节点,那这个节点将作为工作流的起始节点。

若是Start也不存在,则会从所有的Activities中查找第一个Start节点,若存在,则作为工作流起始节点。

若在Activities中也没有Start节点,则再判断一下是否有节点勾选了Start Of Workflow选项,若是勾选了,则获取第一个勾选的Activity作为起始节点。



若是再没有符合条件的节点,则会尝试获取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的逻辑比较复杂,我们放到下一篇文章再继续讲解。

Elsa V3学习之Flowchart详解(上)的更多相关文章

  1. iOS学习之UINavigationController详解与使用(一)添加UIBarButtonItem

    http://blog.csdn.net/totogo2010/article/details/7681879 1.UINavigationController导航控制器如何使用 UINavigati ...

  2. [转]iOS学习之UINavigationController详解与使用(三)ToolBar

    转载地址:http://blog.csdn.net/totogo2010/article/details/7682641 iOS学习之UINavigationController详解与使用(二)页面切 ...

  3. 各大公司广泛使用的在线学习算法FTRL详解

    各大公司广泛使用的在线学习算法FTRL详解 现在做在线学习和CTR常常会用到逻辑回归( Logistic Regression),而传统的批量(batch)算法无法有效地处理超大规模的数据集和在线数据 ...

  4. 从51跳cortex-m0学习2——程序详解

    跳cortex-m0——思想转变>之后又一入门级文章,在此不敢请老鸟们过目.不过要是老鸟们低头瞅了一眼,发现错误,还请教育之,那更是感激不尽.与Cortex在某些操作方式上的异同,让自己对Cor ...

  5. [js高手之路]深入浅出webpack教程系列2-配置文件webpack.config.js详解(上)

    [js高手之路]深入浅出webpack教程系列索引目录: [js高手之路]深入浅出webpack教程系列1-安装与基本打包用法和命令参数 [js高手之路]深入浅出webpack教程系列2-配置文件we ...

  6. iOS学习之UINavigationController详解与使用(三)ToolBar

    1.显示Toolbar  在RootViewController.m的- (void)viewDidLoad方法中添加代码,这样Toobar就显示出来了. [cpp] view plaincopy [ ...

  7. Hadoop深入学习:MapTask详解

    转自:http://flyingdutchman.iteye.com/blog/1878775#bc2337280 Hadoop深入学习:MapTask详解 博客分类: Hadoop MapTask执 ...

  8. Flink 从 0 到 1 学习 —— Flink 配置文件详解

    前面文章我们已经知道 Flink 是什么东西了,安装好 Flink 后,我们再来看下安装路径下的配置文件吧. 安装目录下主要有 flink-conf.yaml 配置.日志的配置文件.zk 配置.Fli ...

  9. SSL/TLS协议详解(上):密码套件,哈希,加密,密钥交换算法

    本文转载自SSL/TLS协议详解(上):密码套件,哈希,加密,密钥交换算法 导语 作为一名安全爱好者,我一向很喜欢SSL(目前是TLS)的运作原理.理解这个复杂协议的基本原理花了我好几天的时间,但只要 ...

  10. [转]iOS学习之UINavigationController详解与使用(二)页面切换和segmentedController

    转载地址:http://blog.csdn.net/totogo2010/article/details/7682433 iOS学习之UINavigationController详解与使用(一)添加U ...

随机推荐

  1. 在Linux中使用crontab

    背景 虽然不是专业运维,但是在嵌入式开发中还是需要懂一点的.部门内部搞服务器最厉害的就是我了,汗. 参考: https://blog.csdn.net/longgeaisisi/article/det ...

  2. RabbitMQ 3.7.9版本中,Create Channel超时的常见原因及排查方法

    在RabbitMQ 3.7.9版本中,Create Channel超时的常见原因及排查方法如下: 常见原因 网络问题: 网络延迟或不稳定可能导致通信超时. 网络分区(network partition ...

  3. 基于 .net core 8.0 的 swagger 文档优化分享-根据命名空间分组显示

    前言 公司项目是是微服务项目,网关是手撸的一个.net core webapi 项目,使用 refit 封装了 20+ 服务 SDK,在网关中进行统一调用和聚合等处理,以及给前端提供 swagger ...

  4. mac 安装jdk1.8 附详细教程

    详细步骤 下载 链接: https://pan.baidu.com/s/1xQr6_9_7lFNtSes7HsKveA 密码: edme 安装包 一直按继续 配置系统环境变量 上一步骤,实标上,我们只 ...

  5. Swift开发基础06-闭包

    Swift的闭包(Closures)是一种将功能块和上下文整合并演示在代码中的一种手段.闭包可以捕获并存储其上下文中的变量和常量.与普遍存在于其他语言的匿名函数(如Python的lambda.Java ...

  6. 基于 Vagrant 手动部署多个 Redis Server

    环境准备 宿主机环境:Windows 10 虚拟机环境:Vagrant + VirtualBox Vagrantfile 配置 首先,我们需要编写一个 Vagrantfile 来定义我们的虚拟机配置. ...

  7. oeasy教您玩转vim - 91 - # vim脚本编程展望

    ​ vim脚本编程展望 回忆 上次我们彻底研究了vim高亮的原理 各种语法项syntax item 关键字keyword 匹配模式match 区域region 定义好了之后还可以设置链接成组 hi d ...

  8. 学习笔记--区分Java中类与对象

    区分Java中类与对象 你能接受 String 吗? 如果能,以下内容可进行类比 编写 User 类 public class User{ // 用户编号 // int 是一种基础数据类型 // id ...

  9. manim边学边做--Table

    表格是一种常见的数据展示形式,manim提供了Table模块专门用于显示表格形式的数据.表格Table和上一节介绍的矩阵Matrix都是用来显示二维数据的,不过,Table的表现力更强,比如,它可以显 ...

  10. Windows cifs共享给linux

    Windows 搜索启用或关闭Windows功能 启用cifs 共享文件并添加普通用户 解禁guest用户和设置本地策略 右键计算机 win+r输入secpol.msc 将guest删除 修改网络访问 ...