- 后端
- 工作流自动化
- 流程编排
【免费下载链接】workflow-core
Lightweight workflow engine for .NET Standard
导读
本文围绕 workflow-core 官方示例 Sample05(README)展开,深入讲解“延迟执行”(Deferred Execution)这一核心特性:如何让工作流的某一条执行路径“睡一会儿”,在指定时间后再自动恢复运行。读完本文,你将掌握ExecutionResult.Sleep的调用方式与持久化语义、内置Delay步骤的 Fluent 与 JSON/YAML 用法,并理解休眠背后的调度原理(SleepUntil、NextExecution与轮询器),可直接在自己的项目中实现定时暂停、冷却等待、分批处理等场景。
什么是延迟执行
在 workflow-core 中,工作流的每一步都由IStepBody执行,并通过返回ExecutionResult来告诉引擎“下一步做什么”。大多数步骤返回ExecutionResult.Next()表示立即推进,但引擎还提供了一种特殊的执行结果——延迟执行(Deferred Execution):
- 步骤第一次运行时,返回一个“休眠”结果,引擎将该执行指针(
ExecutionPointer)标记为休眠状态,并在内部记录一个“唤醒时间”; - 在该时间到来之前,该指针不会被再次执行;
- 时间一到,引擎自动重新调度该步骤,此时步骤再次执行,并可根据持久化数据(
PersistenceData)判断自己处于“第二次经过”,从而返回正常结果让工作流继续前进。
正如示例 README 所说:这个特性由步骤的执行结果来触发——第一步先返回一个SleepResult,第二次经过时再返回普通的OutcomeResult。
核心 API:ExecutionResult.Sleep
延迟执行的入口是 ExecutionResult.cs 中定义的静态工厂方法:
public static ExecutionResult Sleep(TimeSpan duration, object persistenceData) { return new ExecutionResult { Proceed = false, SleepFor = duration, PersistenceData = persistenceData }; }它构造了一个包含三个关键属性的结果:
| 属性 | 值 | 含义 |
|---|---|---|
Proceed | false | 不推进执行指针,即当前分支在此暂停 |
SleepFor | TimeSpan | 需要休眠的时长 |
PersistenceData | 任意对象 | 挂起到该步骤上的持久化数据,唤醒后可读回 |
除了静态方法,抽象基类 StepBody.cs 还提供了受保护的同义助手方法SleepResult(object persistenceData, TimeSpan sleep),供自定义步骤内部使用。
需要特别强调的是:休眠不是靠阻塞线程实现的。引擎不会让步骤所在线程原地Thread.Sleep,而是通过记录“唤醒时间 + 持久化数据”的方式把该分支挂起,再由后台轮询器在合适时机将其唤醒——这一点在“运行时调度原理”一节会详细展开。
官方示例:SleepStep 两次经过模式
示例 README 给出了一个典型的“两次经过”写法(完整源码见 SleepStep.cs):
public class SleepStep : StepBody { public TimeSpan Period { get; set; } public override ExecutionResult Run(IStepExecutionContext context) { if (context.PersistenceData == null) return ExecutionResult.Sleep(Period, new object()); else return ExecutionResult.Next(); } }这段代码的核心逻辑是用PersistenceData判断这是第几次经过该步骤:
- 第一次经过:
context.PersistenceData为null(引擎传入该指针上尚未挂任何数据),于是返回ExecutionResult.Sleep(Period, new object()),请求休眠Period时长,并挂上一个非 null 的持久化标记(这里用new object()仅作为“已休眠过”的哨兵,也可以换成任何需要跨休眠保留的业务数据); - 第二次经过:休眠到期后引擎重新执行该步骤,此时
context.PersistenceData中读回的就是第一次挂上的对象,非 null,于是返回ExecutionResult.Next(),让执行指针正常推进到下一步。
这正是延迟执行与普通“等待”的本质区别:同一段步骤代码在不同轮次返回不同的执行结果,而状态就保存在PersistenceData中。
完整工作流与运行入口
示例 README 展示了将SleepStep编排进工作流的方式(完整源码见 DeferSampleWorkflow.cs):
public class DeferSampleWorkflow : IWorkflow { public string Id => "DeferSampleWorkflow"; public int Version => 1; public void Build(IWorkflowBuilder<object> builder) { builder .StartWith(context => { Console.WriteLine("Workflow started"); return ExecutionResult.Next(); }) .Then<SleepStep>() .Input(step => step.Period, data => TimeSpan.FromSeconds(20)) .Then(context => { Console.WriteLine("workflow complete"); return ExecutionResult.Next(); }); } }要点拆解:
- 工作流以
IWorkflow接口 +Build(IWorkflowBuilder<object>)定义,包含Id("DeferSampleWorkflow")与Version(1); - 链路为:
StartWith(打印 "Workflow started")→Then<SleepStep>→Then(打印 "workflow complete"); - 通过
.Input(step => step.Period, data => TimeSpan.FromSeconds(20))把SleepStep.Period注入为20 秒——这是 workflow-core Fluent API 的标准输入映射方式,表达式左侧是步骤属性,右侧是从工作流数据(data)中求值的表达式; - 运行结果:启动后约 20 秒内停留在
SleepStep,到期后自动推进并打印 "workflow complete"。
配套的宿主启动代码见 Program.cs,展示了如何注册并启动工作流:
IServiceProvider serviceProvider = ConfigureServices(); // 启动工作流宿主 var host = serviceProvider.GetService<IWorkflowHost>(); host.RegisterWorkflow<DeferSampleWorkflow>(); host.Start(); // 启动一个工作流实例(此处为演示,实际生产中通常由其它进程触发) host.StartWorkflow("DeferSampleWorkflow", 1, null, null);依赖注入部分使用services.AddWorkflow()(默认单节点内存实现),并且注释中保留了接入 SqlServer 持久化的写法services.AddWorkflow(x => x.UseSqlServer(...))——当使用真实持久化提供程序时,休眠状态在进程重启后依然可恢复(见下文“持久化”一节)。
运行时调度原理:SleepUntil 与轮询器
延迟执行之所以能做到“非阻塞、可恢复”,是因为引擎将“休眠”落地成了三个数据状态。下面按调用链从源码逐一印证:
1. 处理步骤结果时写入唤醒时间。步骤返回ExecutionResult.Sleep(...)后,由 ExecutionResultProcessor.cs 处理:
pointer.PersistenceData = result.PersistenceData; pointer.Outcome = result.OutcomeValue; if (result.SleepFor.HasValue) { pointer.SleepUntil = _datetimeProvider.UtcNow.Add(result.SleepFor.Value); pointer.Status = PointerStatus.Sleeping; }即:把挂起数据写入ExecutionPointer.PersistenceData,把SleepUntil计算为“当前 UTC 时间 + 休眠时长”,并将指针状态置为PointerStatus.Sleeping(枚举定义见 ExecutionPointer.cs)。
2. 执行器只挑选“到期”的指针。WorkflowExecutor.cs 在每一轮执行时过滤出可执行的指针:
var exePointers = new List<ExecutionPointer>(workflow.ExecutionPointers .Where(x => x.Active && (!x.SleepUntil.HasValue || x.SleepUntil < _datetimeProvider.UtcNow)));因此处于休眠中的指针虽然保持Active,但在SleepUntil到达前不会被选中执行。
3. 计算工作流的下一次执行时间。同文件中的 DetermineNextExecutionTime 会遍历所有休眠指针,把workflow.NextExecution收敛为最早的SleepUntil(ticks)。休眠中的工作流实例不会反复空转执行,而是得到一个精确的下次唤醒时间点。
4. 轮询器负责把到期实例捞回队列。后台任务 RunnablePoller.cs 以固定周期(默认PollInterval = TimeSpan.FromSeconds(10),见 WorkflowOptions.cs)轮询持久化层:
var runnables = await _persistenceStore.GetRunnableInstances(_dateTimeProvider.Now);以内存实现为例,MemoryPersistenceProvider.cs 的判断逻辑是:
return _instances.Where(x => x.NextExecution.HasValue && x.NextExecution <= now).Select(x => x.Id).ToList();捞回的实例 ID 会被投入工作流队列,由WorkflowConsumer消费并交给WorkflowExecutor重新执行,此时SleepStep第二次经过,返回ExecutionResult.Next(),分支继续前进。
精度说明:休眠唤醒的精度受
PollInterval(默认 10 秒)影响,即唤醒时间存在最多一个轮询周期的延迟。对秒级精度敏感的场景,可通过services.AddWorkflow(x => x.UsePollInterval(TimeSpan.FromSeconds(1)))调小轮询间隔(代价是轮询压力增大)。集成测试 DelayScenario.cs 正是把等待时长设置为Host.Options.PollInterval.Add(TimeSpan.FromSeconds(1))来验证该机制的。
更便捷的写法:内置 Delay 步骤
如果只是想在两个步骤之间插入一段固定延时,不必自己写SleepStep——workflow-core 内置了 Delay.cs 步骤,其实现与示例本质相同:
public class Delay : StepBody { public TimeSpan Period { get; set; } public override ExecutionResult Run(IStepExecutionContext context) { if (context.PersistenceData != null) return ExecutionResult.Next(); return ExecutionResult.Sleep(Period, true); } }Fluent API:Fluent 构建器提供了专门的方法,见 StepBuilder.cs:
builder .StartWith(context => Step1Ticker++) .Delay(data => data.WaitTime) // 从工作流数据中读取 TimeSpan .Then(context => Step2Ticker++);Delay(Expression<Func<TData, TimeSpan>> period)会创建一个WorkflowStep<Delay>并把表达式映射到Delay.Period属性。
JSON / YAML:使用外部定义(DSL)时,Delay步骤同样可用,参见 docs/control-structures.md 中的定义:
{ "Id": "MyDelayStep", "StepType": "WorkflowCore.Primitives.Delay, WorkflowCore", "NextStepId": "...", "Inputs": { "Period": "<<expression to evaluate>>" } }Id: MyDelayStep StepType: WorkflowCore.Primitives.Delay, WorkflowCore NextStepId: "..." Inputs: Period: "<<expression to evaluate>>"注意 JSON/YAML 定义中StepType必须使用完整程序集限定名WorkflowCore.Primitives.Delay, WorkflowCore,Period接收一个可求值的表达式。
持久化与恢复
延迟执行的“状态”实际存放在执行指针上,因此是否可跨进程/重启恢复,取决于所选的持久化提供程序:
- 默认的
TransientMemoryPersistenceProvider(单节点内存)适合开发与测试,进程重启后实例会丢失; - 接入 providers 目录下的真实持久化实现(如 SqlServer、PostgreSQL、MongoDB、MySQL、Redis、RavenDB 等)后,
SleepUntil、PersistenceData、Status = Sleeping都会被序列化保存,工作流实例在服务重启后仍能在到期时被轮询器重新捞起执行; - 与此相关,仓库对各个持久化实现都提供了集成测试(例如 WorkflowCore.Tests.SqlServer、WorkflowCore.Tests.PostgreSQL 中的
DelayScenario系列),可验证延迟执行在不同存储下的行为。
常见应用场景与注意事项
典型场景
- 定时/延迟操作:步骤完成 N 秒后再执行后续动作(如发送提醒、释放资源);
- 限速与冷却:在重试/轮询类流程中插入等待窗口,避免对下游系统造成压力;
- 分批处理:与
Foreach、While等控制结构组合,实现分批执行、批次间休眠; - 等待窗口:结合
PersistenceData保存待汇总数据,在唤醒后继续累加处理。
注意事项
- 休眠不占线程:引擎通过“唤醒时间戳 + 持久化数据”实现挂起,长时休眠(如 12 小时)也不会阻塞任何工作线程,这是与
Thread.Sleep的本质区别; - 两次经过是约定而非强制:示例用
PersistenceData == null判断首轮,你可以用任何可序列化对象(如业务快照)代替new object()哨兵,唤醒后直接读取使用; - 唤醒精度受 PollInterval 约束:默认 10 秒轮询,需要更精确的唤醒时间可调整
UsePollInterval; - 状态需可序列化:
PersistenceData会被持久化存储,请确保挂起的对象可被所选提供程序正确序列化; - 执行顺序保障:休眠指针在到期前保持
Active但不会被执行,到期后由调度链自动续跑,分支会从SleepStep的下游继续,而不会重复执行已完成的前置步骤。
参考资料
- 示例文档:src/samples/WorkflowCore.Sample05/README.md
- 步骤实现:SleepStep.cs 与 DeferSampleWorkflow.cs
- 宿主启动:Program.cs
- 核心 API:ExecutionResult.cs、StepBody.cs
- 调度实现:WorkflowExecutor.cs、ExecutionResultProcessor.cs、RunnablePoller.cs
- 内置步骤:Delay.cs 及 StepBuilder.cs
- DSL 定义:docs/control-structures.md
- 验证测试:DelayScenario.cs
- 后端
- 工作流自动化
- 流程编排
【免费下载链接】workflow-core
Lightweight workflow engine for .NET Standard
相关推荐
fre:ac 免费开源音频转换器完整指南:从CD抓轨到批量转码的实战手册
fre:ac 免费开源音频转换器完整指南:从CD抓轨到批量转码的实战手册 还在为下载的 FLAC 无损音频在手机上无法播放而发愁?还在为一摞老旧 CD 无法数字
音视频桌面应用BenchmarkDotNet 基准测试中的延迟执行陷阱:Deferred Execution 校验与 Consume 的正确用法
BenchmarkDotNet 基准测试中的延迟执行陷阱:Deferred Execution 校验与 Consume 的正确用法 导读 LINQ 查询默认是延
性能测试开发工具Workflow Core 1.2.8 版本特性解读:Schedule 定时分支、Delay 延迟执行与内联步骤实战
Workflow Core 1.2.8 版本特性解读:Schedule 定时分支、Delay 延迟执行与内联步骤实战 导读 :本文以 Workflow Core
后端工作流自动化流程编排
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考