☰
workflow-core 延迟执行(Deferred Execution)实战指南:用 ExecutionResult.Sleep 让工作流分支按需休眠
2026/10/12 2:00:56 网站建设 项目流程
  • 后端
  • 工作流自动化
  • 流程编排

【免费下载链接】workflow-core

Lightweight workflow engine for .NET Standard

项目地址:https://gitcode.com/gh_mirrors/wo/workflow-core
点击查看免费下载

导读

本文围绕 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 }; }

它构造了一个包含三个关键属性的结果:

属性值含义
Proceedfalse不推进执行指针,即当前分支在此暂停
SleepForTimeSpan需要休眠的时长
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判断这是第几次经过该步骤:

  1. 第一次经过:context.PersistenceData为null(引擎传入该指针上尚未挂任何数据),于是返回ExecutionResult.Sleep(Period, new object()),请求休眠Period时长,并挂上一个非 null 的持久化标记(这里用new object()仅作为“已休眠过”的哨兵,也可以换成任何需要跨休眠保留的业务数据);
  2. 第二次经过:休眠到期后引擎重新执行该步骤,此时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保存待汇总数据,在唤醒后继续累加处理。

注意事项

  1. 休眠不占线程:引擎通过“唤醒时间戳 + 持久化数据”实现挂起,长时休眠(如 12 小时)也不会阻塞任何工作线程,这是与Thread.Sleep的本质区别;
  2. 两次经过是约定而非强制:示例用PersistenceData == null判断首轮,你可以用任何可序列化对象(如业务快照)代替new object()哨兵,唤醒后直接读取使用;
  3. 唤醒精度受 PollInterval 约束:默认 10 秒轮询,需要更精确的唤醒时间可调整UsePollInterval;
  4. 状态需可序列化:PersistenceData会被持久化存储,请确保挂起的对象可被所选提供程序正确序列化;
  5. 执行顺序保障:休眠指针在到期前保持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

项目地址:https://gitcode.com/gh_mirrors/wo/workflow-core
点击查看免费下载

相关推荐

上一篇:Playnite便携版终极指南:3个创新方案解决跨设备游戏库同步难题
下一篇:3个架构级方案彻底解决跨平台游戏库管理难题

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询