工作流程的微服務:使用F#DSL表達業務流程

banq發表於2019-03-24

我們在Jet上使用F#並且從一開始就是這樣,這就是為什麼在評估構建DSL(領域特定語言)的選項時,F#是一個領先者。當我們決定構建DSL時,我們需要確定DSL有哪些重要的特徵:
  • 編譯時間驗證:由於開發人員主要是構建步驟和定義工作流程,因此我們希望工作流程享受F#提供的型別安全性和工具。
  • 可讀性:工作流是業務邏輯的表示,應該由開發人員和業務使用者輕鬆閱讀。
  • 擴充套件性:任何好的DSL都允許擴充套件或改進,而不會破壞或影響現有的實現。我們希望將來能夠輕鬆新增新型別的步驟和組合。

我們最終確定的工作流DSL主要圍繞鏈/組合步驟的能力。每一步都只是一個函式,它給出一個輸入和狀態產生一個輸出/副作用(Input → State → Output/SideEffect)。這些步驟可以組合或連結在一起以表示複雜的業務流程。
流程的視覺化表示是工程師和業務使用者可以一起設計和理解,一旦確定了流的視覺化表示,開發人員就可以使用DSL來定義工作流,使用以下函式表示:

workflow : (name : string) -> 
    (triggers : Trigger list) -> 
    (metadata : WorkflowMetadata) -> 
    (step : WorkflowStep) -> 
    Workflow


比如一個示例工作流程:建立訂單,預留庫存,傳送訂單,然後最終向客戶收費。這可以使用上面的工作流程函式編寫:

workflow "SampleWorkflow"
    [Trigger.Stream ("kafka://jetkafka/mock-input", TaskIdType.FromPath(Path.JsonPath("$.orderId")), Path.JsonPath("$.orderId"))]
    metadata
    (step("CreateOrder", "CreateOrder") =>
        step("ReserveInventory", "ReserveInventory") => 
            step("ShipOrder", "ShipOrder") =>>
                [
                    step("ChargeCustomer", "ChargeCustomer") =?>
                        [
                            cond("WriteChargeSuccess", "WriteChargeSuccess", Condition.Simple(Qualifier.State, "$.transactionSuccess", "true"))
                            cond("WriteChargeFailure", "WriteChargeFailure", Condition.Simple(Qualifier.State, "$.transactionSuccess", "false"))
                        ]
                    step("UpdateOrderHistory", "UpdateOrderHistory")
                ]
    )

我將在下面更詳細地討論這個DSL的一些元素。但是,我想強調該workflow功能的主要特點:
  1. 工作流名稱(workflow "Sample Workflow"):工作流都需要一個名稱,這些名稱負責許多標識元素以及控制元素,如執行通道控制,啟用/禁用鍵名等。
  2.  觸發:使用Discriminated Union指定觸發器,其中每種情況對應於特定的輸入型別/源。常見的觸發器是一個Stream有三個引數string * TaskIdType * PrimaryKeyPath的字串:字串是StreamDefinition,而TaskIdType和PrimaryKeyPath是觸發器訊息的JsonPath。注意:工作流可以有多個觸發器,並且具有各種型別。
  3. 工作流後設資料:工作流後設資料定義了與工作流執行器,副作用執行器以及失敗的工作流/副作用應該在哪裡進行通訊的通道,以及各種配置元素,如工作流的併發設定。(有關更多架構詳細資訊,請參閱我之前釋出的微服務到工作流程
  4. 步驟:步驟表示需要執行的操作或副作用。這些操作是顯示名稱和實現模組名稱的元組。注意:在此示例中,顯示名稱和步驟實現是相同的,但是在工作流程中多次重複使用相同步驟實現的情況將導致不同的顯示名稱。
  5. 步驟組成:透過一系列組合功能組合步驟以形成工作流程。更多關於後者的內容。


觸發器
工作流可以有一個或多個不同的觸發器。這些觸發器用於配置WorkflowTriggers服務從哪裡使用觸發器訊息。這種消費與我們的微服務中使用的消費機制相同,這在以前的帖子F#Microservice Patterns @ Jet.com中有詳細介紹。的觸發的型別是可區分聯合(代數型):

type Trigger =
| Simple of string * TaskIdType
| Stream of string * TaskIdType * PrimaryKeyPath
...


注意:我不打算在這篇文章中詳細介紹所有這些不同型別的觸發器,我只討論Simple和Stream。我們目前支援其他幾個在處理包含多種不同訊息型別的觸發器流時非常有用的方法。
Stream上面示例工作流程中使用的觸發器有三個引數:
  • string :string這裡表示流定義,它是外部系統的表示(即KafkaEventStore,API等)。之前的文章在F#中抽象IO有關這個概念以及我們如何在整個系統中建模和使用它們的詳細資訊。
  • TaskIdType :每次執行工作流都有唯一的任務ID。該TaskIdType指定如何的taskid獲得。目前,我們支援多種型別的任務ID。我們支援隨機生成的這些,當您不希望進行任何形式的重複資料刪除檢查並始終希望執行工作流時,可以使用這些。我們還支援從輸入有效負載中提取任務ID,以確保每個訊息只執行一次工作流。
  • PrimaryKeyPath :是一個JSON路徑,用於從哪裡提取要用作日記帳ID的值。Journal是所有工作流程的真實來源,它是工作流程所採取的每個操作的事件源日誌。


Simple觸發型別是消費為每個輸入一個新的金鑰指定的流定義的觸發器。工作流輸入的工作流例項ID設定為新GUID。當傳入資料沒有與之關聯的唯一識別符號時,將使用此觸發器。

步驟
步驟表示工作流需要採取的操作。步驟是任何工作流程的基礎,也是定義其功能的基礎。目前有三種不同型別的步驟:

  • Simple Steps(step):只表示一個動作,將始終執行,並且所有業務邏輯在步驟本身中都是自包含的。使用該step函式表示簡單步驟。
  • 條件步驟(cond):包含a的步驟Condition,這些條件允許將行為資訊編碼到工作流DSL中,以根據特定邏輯決定執行哪些步驟。
  • 可選步驟(option):步驟包含條件但不需要具有負面情況的路徑。條件步驟總是需要有正面和負面的路徑。


謂詞邏輯
謂詞邏輯又名條件,可以在任一使用cond或option步驟或在觸發流用於條件過濾。透過提取資料的工作條件規定的Qualifier基礎上一些JSON路徑,然後他們比較這與提取基於該預期值資料Comparison和OperandType 。支援的不同型別的條件是:

type Condition =
| True
| False
| Simple of Qualifier * path:string * expected:string
| Match of Qualifier * path:string * expectedRegx:string
| Compare of qualifier:Qualifier * typ:OperandsType * path:string * comparison:Comparison * expected:string
| Count of qualifier:Qualifier * path:string * comparison:Comparison * expected:string
| Exists of qualifier:Qualifier * path:string
| Not of Condition
| And of Condition List
| OR of Condition List


這些基本條件解釋如下:
  • True | False:始終評估(True)或永不評估(False)
  • Simple: 從指定的資料中提取資料與預期值進行比較。
  • Match: 根據路徑從指定的限定符中提取資料,並使用正規表示式匹配預期值。
  • Compare: 提取基於路徑上的指定的資料,並與預期值比較,基於Comparison cast 到在OperandsType,Comparison型別目前支援以下比較:

    type Comparison =
    | GreaterThan
    | GreaterThanOrEqual
    | LesserThan
    | LesserThanOrEqual
    | NotEqual
    | Equal
    

OperandsType是常見的資料型別,目前支援的型別是:

type OperandsType =
| Int
| Long
| Double
| Decimal
| Boolean
| DateTime

  • Count: 據路徑(通常是陣列)從指定的限定符中提取資料,獲取元素的計數,並與期望值(字串化的int值)進行比較。
  • Exists: 根據路徑從指定的限定符中提取資料,檢測是否存在
  • Not | And | Or: 布林運算子是否用於組成條件,即

let cond = Condition.Count(Qualifier.Input, "$.a", Comparison.Equal, "10")
let cond2 = Condition.Simple (Qualifier.Aggregate, "$.bs", "sting")
let cond3 = Condition.Simple (Qualifier.State, "$.a", "20")
let condORAnd = (cond OR (cond2 AND cond3)) |> Condition.Not


步驟組合

step(“CreateOrder”, “CreateOrder”) => 
     step(“PreDealOrder”, “PreDealOrder”)


我們使用合成運算子組合步驟:
  • chain(=>):步驟可以組合(=>)在一起形成一個鏈。因此a => b,意味著a首先執行步驟然後執行步驟b。
  • any(=?>):步驟可以由一組條件步驟組成,其中只執行滿足條件的第一步。因此a =?> [b;c;d],暗示步驟a首先執行。然後,假設步b不符合條件,但步驟c和d滿足條件,則由於c是第一位的,只有c得到執行。
  • every(=??>):步驟可以由一組條件組成,其中執行滿足條件的每個步驟。所以a =??> [b;c;d]意味著一步a最先被執行,那麼,假設步a不符合條件,但步驟c和d滿足條件,那麼這兩個步驟c,並d得到執行。
  • all(=>>):可以在執行所有步驟的組中組合步驟。因此a =>> [b;c;d],意味著a首先執行步驟然後執行所有步驟b,c然後d執行下一步。


執行
F#DSL允許我們輕鬆設計和實現工作流程,但為了便於跨服務執行DSL,我們建立了一個不同的內部工作流表示,以便在我們的後端服務中使用。我們將DSL轉換為DAG(有向無環圖),以將我們的規範DSL與執行時環境分離。我們的服務使用此圖表來實際執行工作流程。此圖表很容易表示為F#型別:

type Name = string
    
    type EdgeData = string
    
    type VertexData<'V> = Name * 'V //'V can be the representation of a step
    
    type Vertex<'V> = 
        { data : VertexData<'V>
          outEdges : Edge<'V> List }
    
    and Edge<'V> = 
        { data : EdgeData
          tail : Vertex<'V> }
    
    type Graph<'V> = 
        { name : Name
          vertices : Vertex<'V> List }


我們的執行時環境使用evaluate函式將我們的F#DSL轉換為Graph : evaluate : workflow:Workflow -> WorkflowEvaluation. 工作流評估只是我們圖形的一個型別包裝器,它允許我們輕鬆查詢用於哪個工作流的圖形:

type WorkflowEvaluation = 
{
   name : string 
   model : Graph<WorkflowStep>  
}


該圖允許我們的服務輕鬆遍歷和理解任何給定工作流的路徑,以及將我們的服務執行時與DSL表示分離,這使我們可能擁有可以編譯到執行時的多個不同的DSL。DAG允許我們今天使用DSL,然後適應任何其他可能的DSL或語言,而無需更改我們的後端。這也使我們能夠開發出一種設計語言,然後可以將其對映回DAG。可以透過圖形UI指定此設計語言,以允許業務使用者使用預定義的塊輕鬆開發工作流。
除了允許我們的後端服務具有自己的執行時表示之外,它還允許我們透過將圖形轉換為點圖形來輕鬆地在UI中視覺化我們的工作流程,點圖形是用於視覺化圖形的通常可接受的格式。

結論
所有這些不同元素的組合構成了我們工作流程方案的基礎。雖然工作流DSL(Netflix ConductorApache Airflow等)允許在執行時定義工作流,但我們發現在編譯時定義工作流並透過源控制流程時,開發人員的實踐會得到改進。
上述F#工作流模型允許透過F#簽名檢查進行工作流驗證,以及能夠執行和預提交工作流步驟和DSL的測試,以確保在部署之前的正確性。透過將我們的工作流DSL與後端執行層分離,我們已經能夠演示以其他語言(javascript)和原始文字表達的工作流定義。
 

相關文章