啥是书签

流程引擎的核心关注点是安排流程,
如:第1步做什么 → 第2步做什么 → 第n步做什么...,
至于各步骤具体是怎么做的,是由你来决定的,这不是流程引擎关注的重点。

流程安排可能会涉及到分叉、并行多个线路执行,但这只是流程安排的一种形式。

另外如果你硬要在步骤中直接把某件事做了也行,这种情况一般是在自定义的Activity中去完成具体的业务功能,通常这种参杂特定业务逻辑的Activity复用性不高罢了。

有时候需要在执行到某个Activity时停住,等待另外某个事情完成后,流程再继续执行。 举个简单的请假审批流程的例子:

  1. 定义流程

    员工发起一个请假单的审核流程,先由部门主管审核,然后部门经理审核,最后流程结束。 流程引擎主要关注流程,即这里的:主管审核 → 部门经理审核,主管审核和经理审核的具体动作可以回调业务系统的api或实现自定义的activity去实现, 比如更新数据库中关联请假单的状态。

  2. 填写表单 这跟流程引擎无关,一般是在业务系统中填写表单提交保存到数据库

  3. 提交流程 在业务系统中,首先将请假单的状态改为:已进入审核流程,此时跟流程引擎无关。
    然后调用流程引擎,发起一个执行请假流程,(通常会将请假单的id作为流程引擎的相关性id参数传入)

  4. 流程引擎执行

    1. [流程引擎] 开始节点啥也没做,只是表明流程开始执行了,这个好像不是必须的
    2. [流程引擎] 流程进入【主管审核】这个节点,关键就在这里,它做两件事情,1创建一个书签使流程卡在这里,不要继续流转了;2回调业务系统实现主管审核的具体逻辑
    3. [业务系统] 在业务系统去做主管审核的事,也就是将状态改为【主管已审核等待经理审核】然后携带书签调用流程引擎,让流程恢复继续执行辖区
    4. [流程引擎] 流程继续执行,一样的道理会通过书签卡住在经理审核步骤,经理审核后流程继续,最终完成。

在执行某个Activity时,若需要先卡这里等着,就创建一个书签,然后等待外部事情做完后,用刚才这个书签告诉流程引擎继续执行后续步骤。

如果把一个流程看成一本书、把流程中的Activity看成书中每一页,那么书签就是跟真实世界的书签差不多,我看到某一页时,需要去做另一件事时,就在当前页插入一个书签,等我忙完回来后 我翻到书签所在也继续看后续的内容。

书签应用基本流程

Activity核心执行方法定义如下:

ValueTask ExecuteAsync(ActivityExecutionContext context);
  1. 当elsa预定义的Activity或我们自定义的Activity执行时,若需要卡住流程则创建书签,调用context.CreateBookmark即可。
  2. 在ExecuteAsync执行完成后,context中的书签会被复制到WorkflowExecutionContext中,由于当前Activity是卡住的,所以当前流程的本次执行就结束(虽然整个流程未结束,还在等下次从书签恢复)
  3. 流程引擎有个书签持久化中间件,会从当前流程的WorkflowExecutionContext中取出书签,并做持久化存储。
  4. 当业务系统完成工作后,携带书签id再次执行此流程时,流程引擎会从持久化中获取书签,并根据书签信息从上次卡住的Activity恢复流程实例继续执行。

携带书签恢复流程继续执行有多种方式,本文【恢复】节会说明。

书签包含哪些信息

上面基本流程提到,书签有两种数据格式,一种是存储在流程执行上下文WorkflowExecutionContext中的书签,它是用Bookmark类表示的;另一种是持久化存储的用StoredBookmark表示。 这两种格式差不多,比较持久化是通过上下文中的转换来的, 下面描述书签的几个关键属性:

属性 描述
id 书签实例的唯一id
WorkflowInstanceId 书签所属的工作流实例的id
ActivityTypeName 书签卡住的Activity类型的名称
ActivityInstanceId 书签卡住的Activity实例的id
Hash 书签的hash值
CorrelationId 相关性id(通常关联到业务系统的业务实体id,如:请假单的id)
Payload 书签数据,不同的activity在创建和恢复书签之间传递数据
Metadata 自定义的一些额外数据
CreatedAt 创建时间

创建书签时,可以在Payload和Metadata存储复杂数据,以便在后续书签恢复时,从书签中获取这些数据,属于一种参数传递手段。

它俩的区别我也没看懂,不过猜测Payload是跟所在Activity要完成的任务的业务相关的,而Metadata应该是对书签本身属性的一种扩充手段。 意思是我们自定义Activity时,通常使用Payload,而流程引擎可能会使用Metadata来实现一些特殊功能。

创建书签

书签是在流程实例执行到某个Activity时创建的。

一些elsa预定义的Activity本身就会创建书签,如:RunTask,当流程流转到 这种类型的节点时,它会创建一个书签,使流程卡在这里,然后会触发一个事件,我们的代码可以订阅此事件执行业务逻辑,之后通过书签使流程继续。

    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// Create bookmark.
var taskName = TaskName.Get(context); //从设计器中配置的任务名称获取任务名称字符串
var identityGenerator = context.GetRequiredService<IIdentityGenerator>();
var taskId = identityGenerator.GenerateId();//创建一个任务的唯一id
var stimulus = new RunTaskStimulus(taskId, taskName);//暂时理解此对象表示一个要执行的任务,作为bookmark的payload存储起来了。
context.CreateBookmark(stimulus, ResumeAsync, includeActivityInstanceId: false);//重点是这里指定了 当有人携带一个书签请求此流程时,将恢复执行的方法 // Dispatch task request. 后续会触发事件,不是描述书签的重点,请查看官方文档
var taskParams = Payload.GetOrDefault(context);
var runTaskRequest = new RunTaskRequest(context, taskId, taskName, taskParams);
var dispatcher = context.GetRequiredService<ITaskDispatcher>(); await dispatcher.DispatchAsync(runTaskRequest, context.CancellationToken);
}
//通过书签恢复此流程时,流程会从这里继续执行下去
private async ValueTask ResumeAsync(ActivityExecutionContext context)
{
var input = context.GetWorkflowInput<object>(InputKey);
context.Set(Result, input);
await context.CompleteActivityAsync();
}

另外就是我们自定义的Activity了,如果需要让流程卡这里,后面再通过书签恢复流程继续,可以在其ExecuteAsync方法中调用 ActivityExecutionContext的CreateBookmark方法创建书签,此方法有多个重载,不过最核心的重载定义如下,注意查看注释:

    public Bookmark CreateBookmark(CreateBookmarkArgs? options = default)
{
var payload = options?.Stimulus;//通过存储参数到这里,以便在通过书签恢复流程时从中获取此数据,属于一种参数传递方式
var callback = options?.Callback;//当后续通过此书签恢复流程继续时,回调这里指定的方法,不指定的话,默认执行流程书签所在Activity的下一个Activity
var bookmarkName = options?.BookmarkName ?? Activity.Type;//书签名称
var bookmarkHasher = GetRequiredService<IStimulusHasher>();
var identityGenerator = GetRequiredService<IIdentityGenerator>();
var includeActivityInstanceId = options?.IncludeActivityInstanceId ?? true;
var hash = bookmarkHasher.Hash(bookmarkName, payload, includeActivityInstanceId ? Id : null);//书签hash值
var bookmarkId = options?.BookmarkId ?? identityGenerator.GenerateId();//书签唯一id var bookmark = new Bookmark(
bookmarkId,
bookmarkName,
hash,
payload,
Activity.Id,
ActivityNode.NodeId,
Id,
_systemClock.UtcNow,
options?.AutoBurn ?? true,
callback?.Method.Name,
options?.AutoComplete ?? true,
options?.Metadata); AddBookmark(bookmark);//存储到当前上下文中
return bookmark;
}

这里注意,可以指定将来流程恢复时,从callback指定的委托处开始执行,否则会直接执行书签所在Activity的下一个Activity。

删除书签

Activity无论是自动完成还是外部调用完成,总会调用ActivityExecutionContext的CompleteActivityAsync方法,内部会清空当前Activity的书签, 之后书签持久化中间件PersistBookmarkMiddleware会删除书签。

书签存储(持久化)

通过上述步骤创建的书签对象开始存储在当前ActivityExecutionContext中,之后会被转存到WorkflowExecutionContext。 elsa的执行引擎类似asp.net core的中间件管道模型,在执行流程 【前/后】 会执行一系列中间件,其中PersistBookmarkMiddleware中间件就是在流程执行后 从WorkflowExecutionContext提取并存储书签到数据库或其它持久化,同时还会触发书签变动事件WorkflowBookmarksIndexed,后续的书签调度会订阅此事件。

PersistBookmarkMiddleware会同时存储新的书签,也会删除已经从WorkflowExecutionContext移除的书签。

调度(后台作业让书签自动恢复)

我们可以携带书签信息主动调用流程引擎来让指定流程实例从书签处继续执行,某些时候可以安排一个后台任务,让书签在指定时间自动恢复执行。 这种在后台安排一个作业,以在指定时间通过书签恢复指定流程实例继续执行的事就是书签调度。

书签调度的基本流程如下:

  1. 在流程定义中放一个Activity节点,它必须是Delay、StartAt、Timer、Cron中的一种
  2. 这种Activity被执行时会创建书签,并在书签的payload携带跟时间相关参数
  3. 根据本文前面说的“存储(持久化)”会保存书签到数据库,并触发WorkflowBookmarksIndexed事件
  4. 事件订阅器ScheduleWorkflows会调用书签调度器IWorkflowScheduler去后台作业中安排书签恢复任务
  5. 安排时从payload中获取安排时间,并调用elsa的后台作业框架安排后台作业。

当后台作业执行时,会根据书签信息恢复流程继续,这个在文章中的“恢复”节描述。

elsa内置的能自动恢复的4种Activity

  • Delay:当执行到此类型的Activity时,内部会创建一个书签,并在延迟指定时长后自动根据此书签恢复。
  • StartAt:当执行到此类型的Activity时,内部会创建一个书签,并在到达指定时间点时自动根据此书签恢复。
  • Timer:当执行到此类型的Activity时,内部会创建一个书签,并在到达指定时间点时自动根据此书签恢复。
  • Cron:与Timer类似,只不过是按Cron表达式的周期携带书签恢复流程执行。

这几个Activity挺特殊,其中Timer、StartAt、Cron是触发器,同时触发器本就是特殊的Activity,而Delay仅仅是普通的Activity

书签调度器

由IBookmarkScheduler表示,默认实现为DefaultBookmarkScheduler,它仅关注前面说的4种类型的Activity创建的书签,书签调度器做如下两件事:

  1. 安排后台作业,在指定时间点执行 根据书签恢复流程实例
  2. 删除指定书签的后台作业的安排

触发器调度仅关注Timer、StartAt、Cron,并且总是调度执行一个新的流程实例;而书签调度除了关注这3个外还多一个Delay,并且总是调度作业,通过书签恢复现有流程实例的执行
触发器是在流程定义发布后调度的;而书签调度是Activity执行后,持久化书签时触发调度的。

Timer、Cron、StartAt

触发器是一个特殊的Activity,以Timer为例,它就是一个触发器,当定义的流程发布时,elsa会从流程中提取所有触发器节点,并保存在数据库中, 以便将来需要触发器时直接从数据库中获取,而不是又去找到流程并提取一次,这也可以称为触发器索引化。

Timer触发器也需要安排回台作业的,以便在固定时间到达时自动触发流程执行,关键在于这里的触发流程执行是每次都会创建新流程实例。

    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
//当前流程被执行,是否是因为当前节点触发的
if(context.IsTriggerOfWorkflow())
{
//若是,则直接完成,以便流程后续节点立即执行
await context.CompleteActivityAsync();
return;
} var clock = context.ExpressionExecutionContext.GetRequiredService<ISystemClock>();
var timeSpan = context.ExpressionExecutionContext.Get(Interval);
var resumeAt = clock.UtcNow.Add(timeSpan); context.JournalData.Add("ResumeAt", resumeAt);
context.CreateBookmark(new TimerBookmarkPayload(resumeAt));
}

若当前流程就是当前触发器自己触发执行的,则直接让当前Activity完成,以便流程后续节点立即执行。if(context.IsTriggerOfWorkflow())就是做这事的。假设Timer是流程首个节点,每隔指定时间, 流程都会被执行,且每次都会创建新的流程实例。

若当前执行不是被Timer自己这个节点触发的,说明流程是流转到此节点的,此时会创建书签,按本文之前的说明,此书签会被安排后台任务,以便在指定时长后自动恢复,此时就跟延迟固定时间后恢复一样,所以Timer在书签中并不会一直执行,而是每次仅仅恢复自己这个流程实例。

书签匹配模式

有时候我们想触发书签恢复流程,但又不想或不方便直接调用api,可以定义一个书签的filter参数(BookmarkQueueItem)来描述我们希望恢复的书签,

书签参数BookmarkQueueItem

它其实定义的时书签filter参数,当系统中出现书签时,若与这里定义的过滤条件匹配时,则自动恢复书签。

入队IBookmarkQueue

默认实现StoreBookmarkQueue

    public async Task EnqueueAsync(NewBookmarkQueueItem item, CancellationToken cancellationToken = default)
{
//使用书签过滤条件描述对象item创建一个书签过滤器
var filter = new BookmarkFilter
{
BookmarkId = item.BookmarkId,
Hash = item.StimulusHash,
WorkflowInstanceId = item.WorkflowInstanceId,
ActivityTypeName = item.ActivityTypeName
};
//尝试立即恢复此书签
var result = await resumer.ResumeAsync(filter, item.Options, cancellationToken);
if (result.Matched)
{
//若确实执行成功说明,说明已经直接恢复书签了,这里直接返回即可
logger.LogDebug("Successfully resumed workflow instance {WorkflowInstance} using bookmark {BookmarkId} for activity type {ActivityType}", item.WorkflowInstanceId, item.BookmarkId, item.ActivityTypeName);
return;
} // There was no matching bookmark yet. Store the queue item for the system to pick up whenever the bookmark becomes present.
logger.LogDebug("No bookmark with ID {BookmarkId} found for workflow {WorkflowInstance} for activity type {ActivityType}. Adding the request to the bookmark queue", item.BookmarkId, item.WorkflowInstanceId, item.ActivityTypeName); var entity = new BookmarkQueueItem
{
Id = identityGenerator.GenerateId(),
WorkflowInstanceId = item.WorkflowInstanceId,
BookmarkId = item.BookmarkId,
StimulusHash = item.StimulusHash,
ActivityInstanceId = item.ActivityInstanceId,
ActivityTypeName = item.ActivityTypeName,
Options = item.Options,
CreatedAt = systemClock.UtcNow,
};
//否则持久化此书签过滤条件,IBookmarkQueueProcessor会在后台线程中来消费这个队列
await store.AddAsync(entity, cancellationToken); // Trigger the bookmark queue processor.
// 触发一个信号,通知消费端,有新的书签恢复请求
await bookmarkQueueSignaler.TriggerAsync(cancellationToken);
}

消费IBookmarkQueueProcessor

IBookmarkQueueWorker是个死循环后台任务,它里面等待书签变动信号,一旦有信号,它就开始调用BookmarkQueueProcessor

    private async Task ProcessItemAsync(BookmarkQueueItem item, CancellationToken cancellationToken = default)
{
//根据参数创建过滤器
var filter = item.CreateBookmarkFilter();
var options = item.Options; logger.LogDebug("Processing bookmark queue item {BookmarkQueueItemId} for workflow instance {WorkflowInstanceId} for activity type {ActivityType}", item.Id, item.WorkflowInstanceId, item.ActivityTypeName);
//尝试恢复书签
var result = await bookmarkResumer.ResumeAsync(filter, options, cancellationToken); if (result.Matched)
{
//若成功则从书签参数队列中删除
logger.LogDebug("Successfully resumed workflow instance {WorkflowInstance} using bookmark {BookmarkId} for activity type {ActivityType}", item.WorkflowInstanceId, item.BookmarkId, item.ActivityTypeName);
await store.DeleteAsync(item.Id, cancellationToken);
}
else
{
//否则,等待下次尝试
logger.LogDebug("No matching bookmark found for bookmark queue item {BookmarkQueueItemId} for workflow instance {WorkflowInstanceId} for activity type {ActivityType}", item.Id, item.WorkflowInstanceId, item.ActivityTypeName);
}
}

书签队列工作者IBookmarkQueueWorker

它内部搞个死循环等信号,信号一来,立即调用IBookmarkQueueProcessor消费。

书签队列信号IBookmarkQueueSignaler

貌似是书签入队与消费之间的一个信号器,在入队后触发这个信号,消费看到信号就马上消费下。 书签本身变动时,也会触发它

队列清理IBookmarkQueuePurger

它也是个后台任务,默认实现是DefaultBookmarkQueuePurger,它不管啥情况,太老的数据直接清理,无论是否成功恢复书签

多种持久化方式IBookmarkQueueStore

内存、ef、dapper等方式

elsa内部的使用场景

直接看哪些地方引用IBookmarkQueue就晓得,下面只说一个 ResumeExecuteWorkflowActivity它订阅事件,当一个流程执行完成时,它尝试去恢复ExecuteWorkflow这个Activity卡住的活动,也就是子流程执行完成后,通知父活动继续。

书签恢复

如论是执行一个新的流程实例,还是使用书签恢复执行一个已有流程实例,最底层的接口都是IWorkflowRunner.RunAsync,只不过若是通过书签恢复流程实例继续执行时传递参数中要包含BookmarkId。

如下方式内部本质都是调用IWorkflowRunner

  • 通过IWorkflowRuntime创建IWorkflowClient,默认实现LocalWorkflowClient(额外模块的实现:DistributedWorkflowClient、ProtoActorWorkflowClient )后调用其RunInstanceAsync
  • 通过IWorkflowStarter的默认实现DefaultWorkflowStarter,内部是调用的IWorkflowRuntime,然后创建IWorkflowClient,最后调用执行流程
  • 通过IBookmarkResumer的默认实现BookmarkResumer,内部也是调用的IWorkflowRuntime,然后创建IWorkflowClient,最后调用执行流程
  • 调用elsa的http api 也可以使用书签恢复流程执行,内部也一样
  • 前面提到的“书签匹配模式”
  • 前面提到的“调度”会自动恢复书签

看命名也懂,若我们要编程的方式恢复,可以优先考虑使用IBookmarkResumer,或者用“书签匹配模式”中的IBookmarkQueue
若我们是独立部署elsa,业务系统与elsa服务器分开的,则可以使用http api方式通过书签通知elsa恢复已有的流程实例。

另外在Activity中创建书签时,若未指定书签恢复时要执行的委托,则自动执行书签所在Activity的下一个Activity,否则就执行这个指定的委托。

总结

略...

.net工作流elsa-书签的更多相关文章

  1. Asp.net工作流workflow实战之书签(二)

    1.winform(web程序)下使用工作流 怎样才能像控制台那样让winform或web页面窗体阻塞等待工作流的继续执行呢 2.BookMark书签 书签:和一般的书签看书的时候方便查看上次看的内容 ...

  2. .NET 5 开源工作流框架elsa技术研究

    今天假期第一天,研究了.NET 5开源工作流框架elsa,现在分享给大家. 一.框架简介 elsa是一个开源的.NET Standard 工作流框架,官方网站:https://elsa-workflo ...

  3. 开源的.Net 工作流引擎Elsa初试——创建工作流服务器和图形化工作流配置管理应用

    微软的Workflow Foundation基于.Net Framework,并且没有向.Net Core迁移的计划.我们的很多项目使用了工作流引擎,这些项目向.Net Core以及更高版本迁移时遇到 ...

  4. 工作流引擎之Elsa入门系列教程之一 初始化项目并创建第一个工作流

    引子 工作流(Workflow)是对工作流程及其各操作步骤之间业务规则的抽象.概括描述. 为了实现某个业务目标,需要多方参与.按预定规则提交数据时,就可以用到工作流. 通过流程引擎,我们按照流程图,编 ...

  5. Asp.net工作流workflow实战之给书签命名(四)

    之前我们的书签名字是通过手动录入的方式,在实际开发中要在流程设计的时候定义好: namespace EazyBPMS.WorkFlow { public sealed class SetStepAct ...

  6. Workflow笔记2——状态机工作流

    状态机工作流 在上一节Workflow笔记1——工作流介绍中,介绍的是流程图工作流,后来微软又推出了状态机工作流,它比流程图功能更加强大. 状态机工作流:就是将工作流系统中的所有的工作节点都可以看做成 ...

  7. 工作流软件是未来web的支柱

    Web 3.0正在敲门,但是开门的人你永远都想不到:工作流软件. 传统上工作流软件是企业级的业务流程管理工具,但现在随着网络互联程度的加深,工作流软件开始呈现出其作为互联网技术的一面. 诚然,社会化媒 ...

  8. 工作流软件如何成为未来web的支柱

    此文作者是 Kevin Lindquist,工作流平台Decisions的营销负责人,原文发表于VB上. Web 3.0 正在敲门,但是开门的人你永远都想不到:工作流软件. 传统上工作流软件是企业级的 ...

  9. 微软 workflow 工作流总结2

    1.公共的状态机工作流 书签的设置 可以在判断模块中的action中赋值,因为在action中肯定要进入到下一个书签,所以可以在此给书签name赋值

  10. 微软 workflow 工作流总结

    1.状态机工作流 (1)状态机工作流从state1 流转到 state2 到stateN (2) state中有执行状态和退出状态 (3) 在进入下一个步骤(state1 到 state2)前,首先会 ...

随机推荐

  1. MFC应用当中,修改tab顺序

    打开资源文件,按"Ctrl+D"组合键,可以查看控件的Tab顺序,使用鼠标顺序点击即可改变Tab顺序,此方法在Visual Studio 2022版本有效,其余版本没有测试过.

  2. 关于总线协议的记忆技巧—i2c、spi

    口诀: 钟高 数下 是开始,(解释,时钟线保持高时,数据线由高拉到低是向下趋势,说明是"开始信号") 钟高 数上 是停止.(解释,时钟线保持高时,数据线由低拉到高是向上趋势,说明是 ...

  3. isPrimitive:检测数据是不是原始数据

    function isPrimitive(value) { return isStatic(value) || typeof value === 'symbol}

  4. 知识分享:Air780E软件之UDP应用示例

    一.UDP概述 UDP(用户数据报协议,UserDatagramProtocol)是一种无连接的.不可靠的传输层协议,主要用于实现网络中的快速通讯.以下是UDP通讯的主要特点: 1.1 无连接通讯: ...

  5. docker实现redis集群

    1.主从模式(Master-Slave) 1.1主从复制原理 主从复制是redis的一种基本的集群方式,它通过将一个Redis节点(主节点)的数据复制到一个或多个其他Redis节点来实现数据的冗余和备 ...

  6. Git commit注释规范

    指定规范的提交格式, 方便后期版本的回滚与记录的查询 格式规范: <type>(<scope>): <subject> // 空一行 <body> 范例 ...

  7. Mybatis【17】-- Mybatis自关联查询一对多查询

    注:代码已托管在GitHub上,地址是:https://github.com/Damaer/Mybatis-Learning ,项目是mybatis-13-oneself-one2many,需要自取, ...

  8. HarmonyOS Next 集成支付宝SDK后无法在模拟器上安装调试的问题

    之前使用模拟器调试都正常,在集成支付宝SDK后,同事说在模拟器上无法安装调试,因为真机资源不够,模拟器不能用实在耽误事,所以就花了点时间研究一下. 报错原因 官方文档的解释 根据文档的说明,应该是cp ...

  9. 构建你的.NET Aspire解决方案

    .NET Aspire 是一组功能强大的工具.模板和包,用于构建可观察的生产就绪应用程序..NET Aspire 通过处理特定云原生问题的 NuGet 包集合提供.云原生应用程序通常由小型互连部分或微 ...

  10. MySql 9 in Docker 利用克隆插件搭建主从

    环境说明 Docker Windows 11 MySql 9.1.0 搭建步骤 1. 准备主库 准备一个主库的配置文件 master.cnf [mysqld] server-id=1 log-bin= ...