squbs-13. 業務流程(orchestrate )DSL

業務流程是服務的主要用例之一,無論你嘗試通過高并發、低延遲業務多服務調用,還是基于各自嘗試業務操作、數據讀寫、服務調用等。簡化描述你的業務邏輯的能力對于服務的易于理解和維護是必不可少的。編排DSL-squbs-pattern的一部分-將使得異步的代碼易于讀寫和理解。

入門

讓我們從一個簡單的但完整的業務流程例子開始。這個業務流程由三個相關的異步任務組成。

  1. 加載請求這個業務流程的查看用戶
  2. 加載item,item的細節可能基于查看用戶
  3. 基于用戶和item數據構建item視圖。

讓我們深入了解流程和細節:

Scala

    // 1. 定義業務流程actor
class MyOrchestrator extends Actor with Orchestrator {

    // 2. 提供初始化expectOnce塊來接收請求消息
  expectOnce {
    case r: MyOrchestrationRequest => orchestrate(sender(), r)
  }
  
    // 3. 定義業務流程(orchestrate )- 業務流程函數.
  def orchestrate(requester: ActorRef, request: MyOrchestrationRequest) {
    
    // 4. 使用pipe(>>)組合業務流在業務邏輯中是需要的
    val userF = loadViewingUser
    val itemF = userF >> loadItem(request.itemId)
    val itemViewF = (userF, itemF) >> buildItemView
    
    // 5. 結束并返回業務流程的結果   
    for {
      user <- userF
      item <- itemF
      itemView <- itemViewF
    } {
      requester ! MyOrchestrationResult(user, item, itemView)
      context.stop(self)
    }
    
    // 6.  通過調用來關閉業務流程
    //    context.stop(self).
  }
  
    // 7. 實現業務流程的具體功能,如下模式:
  def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
    val itemPromise = OPromise[Option[Item]]
  
    context.actorOf(Props[ItemActor]) ! ItemRequest(itemId, seller.id)
  
    expectOnce {
      case item: Item => itemPromise success Some(item)
      case e: NoSuchItem => itemPromise success None
    }
  
    itemPromise.future
  }
  
  def loadViewingUser: OFuture[Option[User]] = {
    val userPromise = OPromise[Option[User]]
    ...
    userPromise.future
  }
  
  def buildItemView(user: Option[User], item: Option[Item]): OFuture[Option[ItemView]] = {
    ...
  }
}

Java

受限于JAVA語言,同樣的實現看起來更加繁瑣:

    // 1. Define the orchestrator actor.
public class MyOrchestrator extends AbstractOrchestrator {

    // 2. Provide the initial expectOnce in the constructor. It will receive the request message.
    public MyOrchestrator() {
        expectOnce(ReceiveBuilder.match(MyOrchestrationRequest.class, r -> orchestrate(r, sender())).build());
    }
  
    // 3. Define orchestrate - the orchestration function.
    public void orchestrate(MyOrchestrationRequest request, ActorRef requester) {
    
        // 4. Compose the orchestration flow as needed by the business logic.
        static CompletableFuture<Optional<User>> userF = loadViewingUser();
        static CompletableFuture<Optional<Item>> itemF = 
            userF.thenCompose(user -> 
                loadItem(user, request.itemId));
        static CompletableFuture<Optional<ItemView>> itemViewF =
            userF.thenCompose(user ->
            itemF.thenCompose(item ->
                buildItemView(user, item)));
    
        // 5. Conclude and send back the result of the orchestration. 
        userF.thenCompose(user ->
        itemF.thenCompose(item ->
        itemViewF.thenAccept(itemView -> {
            requester.tell(new MyOrchestrationResult(user, item, itemView), self());
            context().stop(self());
        })));
    
        // 6. Make sure to stop the orchestrator actor by calling
        //    context.stop(self).
    }
  
    // 7. Implement the orchestration functions as in the following patterns.
    private CompletableFuture<Optional<Item>> loadItem(User seller, String itemId) {
        CompletableFuture<Optional<Item>> itemF = new CompletableFuture<>();
        context().actorOf(Props.create(ItemActor.class))
            .tell(new ItemRequest(itemId, seller.id), self());
        expectOnce(ReceiveBuilder.
            match(Item.class, item ->
                itemF.complete(Optional.of(item)).
            matchEquals(noSuchItem, e ->
                itemF.complete(Optional.empty())).
            build()
        );
        return itemF;
    }
    
    private CompletableFuture<Optional<User>> loadViewingUser() {
        CompletableFuture<Optional<User>> userF = new CompletableFuture<>();
        ...
        return userF.future
    }
    
    private CompletableFuture<Optional<ItemView>> buildItemView(Optional<User> user, Optional<Item> item) {
        ...
    }
}

你可以先放慢閱讀的腳步,深入理解后再閱讀剩余的部分。輕松的閱讀后面的內容并滿足你的好奇心。

依賴

在build.sbt或scala構建文件中加入以下依賴:

"org.squbs" %% "squbs-pattern" % squbsVersion

核心概念

業務流(Orchestrator)

Orchestrator 是一個擴展自actor的特性(trait)來支持業務流程功能。從技術上來講,它是一個 Aggregator 的子特性(trait)并且提供它所有的功能。除此之外,它提供了功能和語法,允許有效的業務流程組合。-加入工具包通常用在創建業務流程函數,在下文中會有詳細的討論。使用orchestrator,actor只需要簡單的擴展 Orchestrator 特性。

import org.squbs.pattern.orchestration.Orchestrator

class MyOrchestrator extends Actor with Orchestrator {
  ...
}

AbstractOrchestrator 是JAVA使用者在java中使用的超類。它組合Actor和Orchestrator,因為JAVA不支持特性mix-in。因此JAVA的orchestrator使用如下:

import org.squbs.pattern.orchestration.japi.AbstractOrchestrator;

public class MyOrchestrator extends AbstractOrchestrator {
    ...
}

與Aggregator相似,orchestrator 通常不聲明Akka actor 接收塊,不過允許expect/expectOnce/unexpect 塊來定義在那些埋點有哪些預期的響應。這些預期塊通常在業務功能流程的內部使用。

Scala: Orchestration Future and Promise

orchestration promise和future與描述在這里scala.concurrent.Futurescala.concurrent.Promise 相似,只是把名字改成OFutureOPromise,意味著他們在actor中將被Orchestrator使用。orchestration的版本通過不存在并發行為的并發版本工件區分自己。它們在它們的簽名中不使用和執行 ExecutionContext(隱式)。它們同樣缺少一些顯式的執行一個異步閉包的函數。在actor中使用,它們的回調不會被actor的作用域之外被調用。這將消除由多線程回調并發修改actor狀態的風險。另外,它們包含性能優化,假設它們始終在actor中被使用。

注意: 不要在actor之外傳遞OFuture。隱私轉換提供scala.concurrent.FutureOFuture之間的轉換。

import org.squbs.pattern.orchestration.{OFuture, OPromise} 

Java: CompletableFuture

Java的CompletableFuture 和他的synchronous 回調用在orchestration中的值占位符。同步回調確保處理future發生在線程的完成時,在orchestration模型中,它將成為orchestrator actor的線程來接收和處理消息。這確保這里不存在同時關閉actor狀態。所有的回調在actor的作用域內進行。

import java.util.concurrent.CompletableFuture;

異步Orchestration功能

Orchestration函數被orchestration流調用來執行單一的orchestration任務,諸如數據庫服務調用。一個orchestration函數必須遵守如下指南:

  1. 它必須使用非Future參數作為入參?;诓l限制。在scala中,參數的數量最高為22。java風格的orchestration不暴露這個限制。在所有的情況下,這類函數不應該含有太多參數。
  2. Scala函數可能從piped (future)輸入被科里化 成分段的直接輸入。在科里化函數中,管道輸入必須為參數的最后一組。
  3. 它必須引發異步執行。異步執行通常通過發送一條被其他actor處理的消息實現。
  4. Scala實現必須返回 OFuture (orchestration future)。Java實現必須返回CompletableFuture

讓我們看一些orchestration函數的例子:

Scala

def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
  val itemPromise = OPromise[Option[Item]]
  
  context.actorOf(Props[ItemActor]) ! ItemRequest(itemId, seller.id)
  
  expectOnce {
    case item: Item => itemPromise success Some(item)
    case e: NoSuchItem => itemPromise success None
  }
  
  itemPromise.future
}

在這個例子中,函數已科里化。itemId 參數被同步傳遞,seller異步傳遞

Java

private CompletableFuture<Optional<Item>> loadItem(User seller, String itemId) {
    CompletableFuture<Optional<Item>> itemF = new CompletableFuture<>();
    context().actorOf(Props.create(ItemActor.class))
        .tell(new ItemRequest(itemId, seller.id), self());
    expectOnce(ReceiveBuilder.
        match(Item.class, item ->
            itemF.complete(Optional.of(item)).
        matchEquals(noSuchItem, e ->
            itemF.complete(Optional.empty())).
        build()
    );
    return itemF;
}

我們從創建OPromise (Scala) 或 CompletableFuture (Java) 保持最終值功能開始。然后我們將ItemRequest發送給另一個actor。這個actor將立刻異步獲取item。一旦我們發送這個請求,我們通過expectOnce注冊一個callback。expectOnce 中的代碼是一個Receive ,它將在ItemActor發回響應時執行。在所有的情況下,它將success promise 或 complete CompletableFuture。在最后,我們發出future。不反悔promise的原因是因為它是可變的。我們不希望在函數外返回一個可變對象。在它上面調用future會提供一個不可變的OPromise視圖,即OFuture。可惜的是,這并不支持JAVA。

下面這個例子在邏輯上與第一個相同,只是將tell替換成了ask:

Scala

private def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
  
  import context.dispatcher
  implicit val timeout = Timeout(3 seconds)
  
  (context.actorOf[ItemActor] ? ItemRequest(itemId, seller.id)).mapTo[Option[Item]]
}

在這種情況下,這個ask(? )操作返回 scala.concurrent.Future,Orchestrator 特性(trait)提供隱式轉換在 scala.concurrent.Future 和OFuture之間,所以ask( ?)從這個函數返回的結果聲明為OFuture類型,且不需要顯示的調用轉換。

Java

private CompletableFuture<Optional<Item>> loadItem(User seller, String itemId) {
    CompletableFuture<Optional<Item>> itemF = new CompletableFuture<>();
    Timeout timeout = new Timeout(Duration.create(5, "seconds"));
    ActorRef itemActor = context().actorOf(Props.create(ItemActor.class));
    ask(itemActor, new ItemRequest(itemId, seller.id), timeout).thenComplete(itemF);
    return itemF;
}

使用 ask?看起來使用更少的代碼,并在 expect/expectOnce中有更差的性能和更少的復雜度。預期塊中的邏輯同樣被用作結果的進一步轉換。這些通過使用ask返回的future同樣可以實現。但是,性能無法簡單的補償,具體原因如下:

  1. Ask將會創建一個新的actor作為響應的接受者
  2. scala.concurrent.FutureOFuture的轉換和JAVA api中的fill操作需要消息 piped 回到orchestrator,因此新增的消息跳躍同時增加了CPU和延遲。

在使用ask而不是expect/expectOnce時,測試顯示更高的延遲和CPU利用率.

組合

Scala

pipe( >>)標記使用至少一個 future OFuture 并且使其結果作為orchestration函數的輸入。當所有OFuture代表的輸入被解決時,實際的orchestration 函數將會異步調用。

pipe是Orchestration DSL 的主要部件,基于他們的輸入輸出,允許orchestration函數被組合。orchestration流被orchestration聲明亦或是通過管道聲明的orchestration 流隱式的指定。

如果多個OFutures輸入到orchestration函數,OFutures 需要逗號分隔和括在括號中,構建OFutures元組作為輸入。在elements元組中的元素數量和OFuture 類型必須匹配函數參數和類型,或者最后一組參數在 科里化情況下或編譯將會失敗。這些錯誤一般會被IDE捕獲。

以下的例子顯示了一個簡單的使用loadItem orchestration函數使用orchestration聲明和流,這些在前面章節已聲明,其中包括:

val userF = loadViewingUser
val itemF = userF >> loadItem(request.itemId)
val itemViewF = (userF, itemF) >> buildItemView

上面的流可以如下描述:

  • 首先調用loadViewingUser (無參)
  • 當viewing user可用時,使用 viewing user 作為入參調用loadItem (在這種情況下,先前的id有效)。loadItem在這種情況下跟隨確切的在orchestration上面聲明的標識符。
  • 當user和item均生效時,調用buildItemView

Java

多個CompletableFuture的組合可以通過使用組合函數CompletableFuture.thenCompose()實現。每個thenCompose 需要已解決的未來的值作為輸入使用lambda。當 CompletableFuture 結束時,它將會被調用。

通過一個使用例子來描述最佳:

static CompletableFuture<Optional<User>> userF = loadViewingUser();
static CompletableFuture<Optional<Item>> itemF = 
    userF.thenCompose(user -> 
        loadItem(user, request.itemId));
static CompletableFuture<Optional<ItemView>> itemViewF =
    userF.thenCompose(user ->
    itemF.thenCompose(item ->
        buildItemView(user, item)));

流程可以描述如下:

  • 首先調用loadViewingUser.
  • 當viewing user生效時,使用viewing user作為參數來調用loadItem (在這種情況下,itemId在之前有效)
  • 當user和item生效時,調用buildItemView

Orchestrator實例生命周期

Orchestrator通常單獨使用actor。他們接受初始化請求,然后基于請求調用的orchestration函數響應發送出去。

為了允許orchestrator服務多個orchestration請求,orchestrator需要為每個請求結合輸入和響應,并且把它們從不同的請求中分離。這會很大程度上加深開發的復雜度,并且在這些我們看到的例子中,它們并不會以一個干凈的orchestration結束 。為此,創建一個新的actor成本更低,為每個orchestration請求我們可以簡單的創建新的orchestrator。

orchestration的最后一部分回調應該關閉actor。在Scala中通過調用context.stop(self)context stop self (如果優先中綴表示法)。Java的實現應該調用:`context().stop(self())。

完成Orchestration流

這里,我們將以上的所有概念結合。從上面開始重復同樣的例子,包含更多完整的解釋:

   // 1. 定義orchestrator actor
class MyOrchestrator extends Actor with Orchestrator {

   // 2.提供初始expectOnce塊,它將接受請求消息
   //    在接收到這些請求后,同樣的actor不會再次接受到同樣的請求。
   //    expectOnce看起來有一個初始化模式匹配請求,并使用請求成員和參數,sender()來調用
   //    高等級orchestration函數。這個函數通常稱為orchestrate。
 expectOnce {
   case r: MyOrchestrationRequest => orchestrate(sender(), r)
 }
 
   // 3. 定義orchestratem,它的參數默認不可變
   //    使得開發者依賴的這些情況永遠不變。
 def orchestrate(requester: ActorRef, request: MyOrchestrationRequest) {
 
   // 4. 如果有任何事我們需要同步啟動orchestration,在orchestrate的最前部分執行

   // 5. 業務邏輯需要使用管道組合orchestration流
   val userF = loadViewingUser
   val itemF = userF >> loadItem(request.itemId)
   val itemViewF = (userF, itemF) >> buildItemView
   
   // 6. 通過調用函數結束流結合了請求響應。如果結合非常大,它可能
   //     具有更多的可讀性來使用結合而不是包含大量的參數的結合函數。
   //     可能在某些特殊響應情況下需要這些多重組合。
   //     例子中展示的組合僅僅作為參考,
   //     在這個小情況下,你可以通過3個參數增加請求者來使用orchestration函數

   for {
     user <- userF
     item <- itemF
     itemView <- itemViewF
   } {
     requester ! MyOrchestrationResult(user, item, itemView)
     context.stop(self)
   }
   
   // 7. 確保最后的響應通過調用關閉orchestrator actor 
   //    context.stop(self).
 }
 
   // 8.在orchestrator actor內實現異步orchestration函數,
   //    但在orchestrate函數之外
 def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
   val itemPromise = OPromise[Option[Item]]
 
   context.actorOf[ItemActor] ! ItemRequest(itemId, seller.id)
 
   expectOnce {
     case item: Item    => itemPromise success Some(item)
     case e: NoSuchItem => itemPromise success None
   }
 
   itemPromise.future
 }
 
 def loadViewingUser: OFuture[Option[User]] = {
   val userPromise = OPromise[Option[User]]
   ...
   userPromise.future
 }
 
 def buildItemView(user: Option[User], item: Option[Item]): OFuture[Option[ItemView]] = {
   ...
 }
}

Java

    // 1. Define the orchestrator actor.
public class MyOrchestrator extends AbstractOrchestrator {

    // 2. Provide the initial expectOnce in the constructor. It will receive the request message.
    public MyOrchestrator() {
        expectOnce(ReceiveBuilder.match(MyOrchestrationRequest.class, r -> orchestrate(r, sender())).build());
    }
  
    // 3. Define orchestrate - the orchestration function.
    public void orchestrate(MyOrchestrationRequest request, ActorRef requester) {

        // 4. If there is anything we need to do synchronously to setup for
        //    the orchestration, do this in the first part of orchestrate.
  
        // 5. Compose the orchestration flow as needed by the business logic.
        static CompletableFuture<Optional<User>> userF = loadViewingUser();
        static CompletableFuture<Optional<Item>> itemF = 
            userF.thenCompose(user -> 
                loadItem(user, request.itemId));
        static CompletableFuture<Optional<ItemView>> itemViewF =
            userF.thenCompose(user ->
            itemF.thenCompose(item ->
                buildItemView(user, item)));
    
        // 6. Conclude and send back the result of the orchestration. 
        userF.thenCompose(user ->
        itemF.thenCompose(item ->
        itemViewF.thenAccept(itemView -> {
            requester.tell(new MyOrchestrationResult(user, item, itemView), self());
            context().stop(self());
        })));
    
        // 7. Make sure to stop the orchestrator actor by calling
        //    context.stop(self).
    }
  
    // 8. Implement the orchestration functions as in the following patterns.
    private CompletableFuture<Optional<Item>> loadItem(User seller, String itemId) {
        CompletableFuture<Optional<Item>> itemF = new CompletableFuture<>();
        context().actorOf(Props.create(ItemActor.class))
            .tell(new ItemRequest(itemId, seller.id), self());
        expectOnce(ReceiveBuilder.
            match(Item.class, item ->
                itemF.complete(Optional.of(item)).
            matchEquals(noSuchItem, e ->
                itemF.complete(Optional.empty())).
            build()
        );
        return itemF;
    }
    
    private CompletableFuture<Optional<User>> loadViewingUser() {
        CompletableFuture<Optional<User>> userF = new CompletableFuture<>();
        ...
        return userF;
    }
    
    private CompletableFuture<Optional<ItemView>> buildItemView(Optional<User> user, Optional<Item> item) {
        ...
    }
}

重用Orchestration 函數

Scala

Orchestration functions通常依賴 Orchestrator trait 提供的功能,無法單獨存在。然而,在許多情況下,跨orchestrator重用orchestration函數來進行不同形式的orchestrate是需要的。在這些情況下,分割orchestration函數至不同的trait并混合每個orchestrator中非常重要。trait需要訪問orchestration并且需要自引用至 Orchestrator。以下是一個簡單的trait例子:

trait OrchestrationFunctions { this: Actor with Orchestrator =>

  def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
    ...
  }
}

上面例子中的 this: Actor with Orchestrator是一個自引用。它告訴Scala編譯器這個triat只能混合到Actor,并且同時是個 Orchestrator,因此它將能訪問ActorOrchestrator功能,并使用這些來自trait和類混合所獲取的功能。

在orchestrator中使用 OrchestrationFunctions trait,只需要如下方式混合這個trait至orchestrator:

class MyOrchestrator extends Actor with Orchestrator with OrchestrationFunctions {
  ...
}

Java

Java 需要一個單獨的層次結構不支持多trait接口集成。重用通過擴展AbstractOrchestrator實現,實現orchestration函數,并且留下剩余的抽象-需要被具體的orchestrator實現,具體如下:

abstract class MyAbstractOrchestrator extends AbstractOrchestrator {

    protected CompletableFuture<Optional<Item>> loadItem(User seller, String itemId) {
        CompletableFuture<Optional<Item>> itemF = new CompletableFuture<>();
        ...
        return itemF;
    }
    
    protected CompletableFuture<Optional<User>> loadViewingUser() {
        CompletableFuture<Optional<User>> userF = new CompletableFuture<>();
        ...
        return userF;
    }
    
    protected CompletableFuture<Optional<ItemView>> buildItemView(Optional<User> user, Optional<Item> item) {
        ...
    }
}

這個具體的orchestrator 實現,只需要從上面的MyAbstractOrchestrator擴展,并實現不同的orchestration。

確保響應唯一

使用 expectexpectOnce時,我們被單個expect塊的模式匹配能力限制,它被限制在作用域中并且不能區別在多orchestration函數中跨expect塊的匹配。接收到的消息來自請求消息 (在同一個orchestration函數聲明expect之前發送)不存在邏輯關系。針對復雜orchestration,我們可能遇到消息混亂的問題。響應并未與正確的請求關聯并且會錯誤處理。這里有一些解決這些問題的策略:

如果是初始消息的接受者,因此響應消息的發送者是唯一的,模式匹配可以包含消息發送者的引用,作為一個如下的模式匹配。

Scala

def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
  val itemPromise = OPromise[Option[Item]]
  
  val itemActor = context.actorOf(Props[ItemActor])
  itemActor ! ItemRequest(itemId, seller.id)
  
  expectOnce {
    case item: Item    if sender() == itemActor => itemPromise success Some(item)
    case e: NoSuchItem if sender() == itemActor => itemPromise success None
  }
  
  itemPromise.future
}

Java

private CompletableFuture<Optional<Item>> loadItem(User seller, String itemId) {
    CompletableFuture<Optional<Item>> itemF = new CompletableFuture<>();
    ActorRef itemActor = context().actorOf(Props.create(ItemActor.class));
    itemActor.tell(new ItemRequest(itemId, seller.id), self());
    expectOnce(ReceiveBuilder.
        match(Item.class, item -> itemActor.equals(sender()), item ->
            itemF.complete(Optional.of(item)).
        matchEquals(noSuchItem, e -> itemActor.equals(sender()), e ->
            itemF.complete(Optional.empty())).
        build()
    );
    return itemF;
}

換句話說,在結合actor實例時,Orchestrator 特性提供唯一消息編號生成器。我們可以使用這個id生成器來生成唯一消息編號。actor接收到這些消息將只需要返回這些消息編號作為響應消息的一部分。下面展示了一個orchestration函數使用消息生成器的例子。

Scala

def loadItem(itemId: String)(seller: User): OFuture[Option[Item]] = {
  val itemPromise = OPromise[Option[Item]]
  
  // Generate the message id.
  val msgId = nextMessageId  
  context.actorOf(Props[ItemActor]) ! ItemRequest(msgId, itemId, seller.id)
  
  // Use the message id as part of the response pattern match. It needs to
  // be back-quoted as to not be interpreted as variable extractions, where
  // a new variable is created by extraction from the matched object.
  expectOnce {
    case item @ Item(`msgId`, _, _) => itemPromise success Some(item)
    case NoSuchItem(`msgId`, _)     => itemPromise success None
  }
  
  itemPromise.future
}

Java

private CompletableFuture<Optional<Item>> loadItem(User seller, String itemId) {
    CompletableFuture<Optional<Item>> itemF = new CompletableFuture<>();
    long msgId = nextMessageId();
    context().actorOf(Props.create(ItemActor.class))
        .tell(new ItemRequest(msgId, itemId, seller.id), self());
    expectOnce(ReceiveBuilder.
        match(Item.class, item -> item.msgId == msgId, item ->
            itemF.complete(Optional.of(item)).
        match(NoSuchItem.class, e -> e.msgId == msgId, e ->
            itemF.complete(Optional.empty())).
        build()
    );
    return itemF;
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容