@
- 定義
- 變數
- 記憶體暫存器類
- 暫存器中的儲存區塊類
- 變數到儲存的對映類
- 上下文物件
- 活動上下文(ActivityExecutionContext)
- 工作流執行上下文(WorkflowExecutionContext)
- 表示式執行上下文(ExpressionExecutionContext)
- 變數
- 構建
- 構建活動
- 構建工作流
- 執行
- 註冊
- 註冊工作流
- 註冊活動
- 填充
- Invoke活動
- 註冊
- 可觀測性
- 設計器與APIs
- 工作流配置
- 檢視工作流狀態
本篇將帶你深入分析Elsa工作流原理,排除干擾展示關鍵程式碼段,以加深理解
定義
變數
Elsa工作原理可以抽象理解為管道中介軟體 + 非同步模型
Elsa中,活動的變數的獲取和設定都是非同步的。Elsa定義了Variable
型別作為非同步操作的結果或者說是非同步操作的佔位符,這個變數在執行的時候才會填充數值。這與我們熟悉C#中的Task,或者js裡的promise物件作用相同。輸入Input,OutPut都屬於 Variable。
Elsa模擬了記憶體暫存器(MemoryRegister)以及Set和Get訪問器實現非同步模型。
記憶體暫存器類
public class MemoryRegister
{
...
public IDictionary<string, MemoryBlock> Blocks { get; }
}
暫存器中的儲存區塊類
public class MemoryBlock
{
...
/// <summary>
/// The value stored in this block.
/// </summary>
public object? Value { get; set; }
/// <summary>
/// Optional metadata about this block.
/// </summary>
public object? Metadata { get; set; }
}
變數到儲存的對映類
Id可以代表變數在記憶體區塊中的引用地址
public class MemoryBlockReference
{
/// <summary>
/// The ID of the memory block.
/// </summary>
public string Id { get; set; } = default!;
public object? Get(MemoryRegister memoryRegister) => GetBlock(memoryRegister).Value;
}
構建活動時將建立活動中變數到儲存區塊的對映,分配一個引用給變數
public void AssignInputOutputs(IActivity activity)
{
var activityDescriptor = _activityRegistry.Find(activity.Type, activity.Version) ?? throw new Exception("Activity descriptor not found");
var inputs = activityDescriptor.GetWrappedInputProperties(activity).Values.Cast<Input>().ToList();
var seed = 0;
foreach (var input in inputs)
{
var blockReference = input?.MemoryBlockReference();
if (blockReference != null!)
if (string.IsNullOrEmpty(blockReference.Id))
blockReference.Id = $"{activity.Id}:input-{seed}";
seed++;
}
...
}
非同步變數獲取和設定:
可以透過上下文物件的Set,和Get方法,非同步獲取和設定非同步變數。
上下文物件
檢視原始碼可以看到Elsa定義瞭如下Context
其中比較重要的上下文物件:
活動上下文(ActivityExecutionContext)
活動上下文物件由Elsa.Runtime提供,在工作流執行函式中可供訪問。透過它可訪問包含活動例項、當前輸入和輸出等。透過它可以訪問當前活動所在的工作流執行上下文。
工作流執行上下文(WorkflowExecutionContext)
工作流上下文物件由Elsa.Runtime提供,可透過活動上下文(ActivityExecutionContext)訪問其所屬工作流執行上下文。透過它可訪問包含工作流例項、當前活動、當前輸入和輸出等。
表示式執行上下文(ExpressionExecutionContext)
表示式執行上下文用於在構建活動時傳遞記憶體變數(輸入,輸出),其中包含MemoryRegister物件。
透過表示式執行上下文(ExpressionExecutionContext)獲取到變數的值:
構建
構建活動
Elsa預設幫我們建立了這些活動:
他們都實現了IActivity介面,Activity和CodeActivity是IActivity的實現類,對應的是一個空的活動,(CodeActivity是帶有自動完成功能的空活動)
我們要做的是繼承這個活動,重寫Execute方法以實現我們自己的業務。比如:
public class HelloWorld : Activity
{
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
Console.WriteLine("Hello World!");
await CompleteAsync();
}
}
以官方預設的WiteLine為例,這個類的Execute程式碼如下:
protected override void Execute(ActivityExecutionContext context)
{
var text = context.Get(Text);
var provider = context.GetService<IStandardOutStreamProvider>() ?? new StandardOutStreamProvider(System.Console.Out);
var textWriter = provider.GetTextWriter();
textWriter.WriteLine(text);
}
構建工作流
首要目標是拿到一個工作流物件(Workflow),Elsa啟動時會從工作流提供者(IWorkflowProvider)獲取所有能用的工作流。並註冊到資源池中
public interface IWorkflowProvider
{
string Name { get; }
ValueTask<IEnumerable<MaterializedWorkflow>> GetWorkflowsAsync(CancellationToken cancellationToken = default);
}
Elsa預設的實現類是如下兩種,BlobStorageWorkflowProvider將從資料庫(BlobStorage)中反序列化來註冊。ClrWorkflowProvider使用工作流構建器註冊。
我們先定義工作流描述類,它繼承自IWorkflow, WorkflowBase是IWorkflow的抽象基類
class SequentialWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
workflow.Root = new Sequence
{
Activities =
{
new WriteLine("Line 1"),
new WriteLine("Line 2"),
new WriteLine("Line 3")
}
};
}
}
Elsa初始化時,WorkflowBuilder會構建程式集中所有實現IWorkflow的類。
WorkflowBuilder中的BuildWorkflowAsync方法會將工作流描述類IWorkflow物件構建成Workflow物件。
這裡思考一個問題:終執行的程式碼是在活動中定義的,但為什麼返回的是Workflow物件?透過程式碼研讀,實際上Workflow也是一個IActivity活動,只不過它具有一個Root根節點的複合活動。活動的定義請參考官方文件
BuildWorkflowAsync中的具體實現如下:
執行
註冊
註冊包括註冊工作流和註冊活動,配置Elsa時需要使用如下兩個方法:
.AddActivitiesFrom<Program>()
.AddWorkflowsFrom<Program>()
註冊工作流
工作流可以透過ClrWorkflowProvider,使用工作流構建器註冊,也可以從本地儲存(BlobStorage)中反序列化來註冊。
程式碼構建的工作流是透過實現IWorkflow介面,在Elsa初始化時將工作流注冊到工作流定義持久化到資料庫的WorkflowDefinition表中
透過工作流構建器註冊:
註冊活動
Elsa使用描述器(IActivityDescriber)提供一個描述符(ActivityDescriptor),這裡比較繞,閱讀原始碼可以發現,其實是透過各種反射獲取活動派生類的特徵資料(有的系統喜歡將稱之為後設資料),封裝這些資料的型別稱之為描述符,特徵資料可以作為在介面上顯示,分組,排序的資訊。
活動不同於工作流,它在執行中不持久化於資料庫,而是以登錄檔的形式儲存於記憶體中。
IDictionary<(string Type, int Version), ActivityDescriptor> _activityDescriptors
在構建工作流的時候自動註冊活動,也可以透過實現IActivity介面,在Elsa初始化時將所有活動註冊到登錄檔中
Elsa啟動時將所有實現了IActivity介面的型別註冊為活動:
填充
啟動時填充活動登錄檔和工作流定義表。
官方也給出了說明,各填充兩次確保活動登錄檔和工作流定義表都是最新的:
階段一:填充活動登錄檔
因為工作流定義可以用作活動,需要確保在填充工作流定義表之前填充活動登錄檔。
階段二:填充工作流定義表
階段三:重新填充活動登錄檔
填充了工作流定義表之後,我們需要重新填充活動登錄檔,以確保活動描述符是最新的。
階段四:用當前的活動集重新更新工作流定義表。
最後,需要重新填充工作流定義表,以確保工作流定義是最新的。
Invoke活動
Elsa預設的管道中介軟體:
Elsa註冊執行活動的中介軟體(DefaultActivityInvokerMiddleware):
public static class ActivityInvokerMiddlewareExtensions
{
/// <summary>
/// Adds the <see cref="DefaultActivityInvokerMiddleware"/> component to the pipeline.
/// </summary>
public static IActivityExecutionPipelineBuilder UseDefaultActivityInvoker(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<DefaultActivityInvokerMiddleware>();
}
在執行活動的中介軟體(DefaultActivityInvokerMiddleware),最終活動被呼叫的程式碼如下:
可以看見,Elsa最終以反射的方式建立一個Activity例項,然後呼叫它的ExecuteAsync方法。
可觀測性
設計器與APIs
實際上,Elsa的執行時和設計器是完全分離的。Elsa提供了一個基於Blazor的設計工具,它作為獨立的專案釋出在Github上: Elsa-Studio
因為和介面互動是透過REST API實現的,所以你也可以使用任何你想要的客戶端來實現。
接設計器預設的HTTP API實現在Elsa.Workflows.Api
庫中,用於支援設計器的增刪改查業務。
如果僅要使用工作流引擎,可以使用Elsa.Workflows.Management
庫,它只包含對於工作流的管理而不涉及HTTP介面。
工作流配置
開啟設計器,點選“工作流(Workflow)”選單,然後單擊“定義(Definition)”選項卡。可以看到一個工作流定義的列表。點選右上角新增按鈕,
在開啟的頁面中,拖拽活動到工作流圖上,然後單擊“儲存(Save)”按鈕。
在瀏覽器的網路請求中可以看到一個POST請求,請求地址為/workflow/definitions,請求引數為JSON格式,後端服務中WorkflowDefinitions的Endpoint中將對編輯器的“儲存”請求進行處理
在請求負載中,WorkflowDefinitionModel欄位會包含工作流定義和Root活動。
預設實現會將工作流定義和根活動序列化為JSON,並將其儲存到資料庫中。其中根活動在資料庫WorkflowDefinition表的StringData列中儲存。
當工作流執行時,Elsa會例項化(Materialize)Workflow物件
其中RootActivity會反序列化,可以看到StringData會被反序列化為IActivity物件
檢視工作流狀態
Elsa定義了不同的介面和資料庫
主要的介面如下:
workflowDefinition:工作流定義介面,資料來自WorkflowDefinition表
workflowInstance:工作流例項介面,資料來自WorkflowInstance表
activity-execution:活動執行介面,查詢活動的Id、狀態以及結果,輸入輸出等上下文資料,資料主要透過查詢ActivityExecutionRecords表來獲取。
journal: 活動執行日誌,資料來自WorkflowExecutionLogRecords表
開啟設計器,點選“工作流(Workflow)”選單,然後單擊“例項(Instance)”選項卡。可以看到一個工作例項列表
點選條目即可檢視工作流的執行日誌和各活動的執行資訊。Web頁面中各片區的資料來源分佈大致如下:
其中頁面中央的工作流編輯器顯示了工作流的結構,結合工作流的執行日誌,可以直觀的看到工作流的執行情況。可觀測到執行的步驟,以及執行的耗時。
--完結--