Camunda 流程引擎的一種 Adapter 層實現

schaepher發表於2020-04-20

上一篇說明了選擇 Camunda 的理由。這一篇說明如何實現適配層。

當前還沒有專門寫一篇對 Camunda 各個功能的詳細介紹。如果要獲得比較直觀的感受,可以下載 Modeler 或者使用線上版的 Modeler 。
https://demo.bpmn.io/

目錄:

  1. 為什麼要做適配層?
  2. 要對 Camunda 做擴充的部分
  3. 資料表的擴充
  4. 流程例項的操作
  5. 前端動態表單的渲染
  6. 結語

為什麼要做適配層?

  • 現有引擎無法滿足業務
    如果能滿足,加個代理層就夠了。
  • 避免改原始碼導致升級困難
    我們部門有個基於 k8s 原始碼修改的專案,當年拿過公司的大獎。現在由於沒人維護的來,已經涼了。
  • 可以相容其他流程引擎
    當前選的引擎即使能滿足當前業務需要,但未必滿足其他業務的需要。更何況要形成一個基礎設施給其他元件使用。
+---------+
|         |
| System  |
|         |
+--+--+---+
   |  ^
   v  |
+--+--+---+
|         |
| Adapter |
|         |
+--+--+---+
   |  ^
   v  |
+--+--+---+
|         |
| Camunda |
|         |
+---------+

要對 Camunda 做擴充的部分

  • 各業務系統有自己的系統介面,也有各自需要展示的內容,不能直接使用 Camunda 自帶的管理介面。

  • Camunda 內建的表單支援使得業務系統對其有一定的依賴。要改成在業務層處理。

  • Camunda 在一次請求跳轉時只能對原任務和目標任務同時應用或者不應用引數檢測。但其實應該要在取消原任務的執行時不檢測引數,在建立目標任務的執行時檢測引數,因此要分兩次。

  • 流程和流程例項 ID 是 UUID,但要展示整數 ID 給使用者。

  • 流程例項自動化任務出錯時,要轉交給運維處理。

  • 制定 External Task Client 的規則

資料表的擴充

為了適應業務需要,需要另外建立幾張資料表:

  • 流程定義資訊表
  • 流程例項資訊表
  • 流程節點日誌表
  • 服務定義表
  • 服務請求客戶端管理表

流程定義資訊表

無論是團隊內部溝通還是和需求方溝通,最經常用到的用於區別流程的資訊是流程的 ID。大家都不太喜歡用流程的名稱來溝通,除非是比較少見的流程。而且在各種文件中也是使用流程的 ID。因此這個 ID 資訊需要展示給使用者。

由於是從舊系統遷移過來的,所以要保證原先流程的 ID 不變。所以不適用自增 ID 作為流程 ID,而是新增一列專門儲存這些 ID。

以下 pd 表示 process definition,e 表示 engine 。

欄位 作用
id 自增 ID
pd_id 流程 ID,相容舊系統
e_pd_key 底層流程引擎的流程名稱,也用於翻譯後作為名稱展示
e_pd_version 當前使用的流程版本號
e_pd_version_max 底層流程引擎的流程最高版本號
category 流程分類
maintainer 維護人
doc_link 文件地址
note 備註

Camunda 可以通過流程名稱和流程版本確定一個特定的流程定義 UUID,所以不需要在這裡加上 UUID。

如果要加入其他引擎,且這些引擎不是用 key 和 version 確定,則可以加入 UUID 欄位。並且還需要加入 engine 欄位用於標識使用哪個引擎。

當新版本釋出時,e_pd_version_max 加一, e_pd_version 保持不變,只能人為修改。這樣便於做測試。

這一部分沒有什麼特別的點,實現起來不難。

流程例項資訊表

pi 表示 process instance

欄位 作用
id 自增 ID,作為流程例項 ID
pd_id 流程 ID
e_pi_id 流程引擎的流程例項 ID,用於與流程引擎的例項保持關聯
e_pd_key 底層流程引擎的流程名稱,也用於翻譯後作為名稱展示
e_pd_version 當前使用的流程版本號
e_pi_activity_name 當前所在的節點名稱
name 流程例項名稱,由建立人填寫
creator 建立人
source 建立來源,其他系統可呼叫 Restful API 建立流程例項
priority 優先順序
handlers 處理人,可以有多個
status 當前流程例項狀態
tags 標籤,作為補充資訊

流程例項狀態:

型別 作用
建立 流程例項剛建立
待提交表單 需要等待使用者提交表單
等待執行器 自動化執行步驟,需要等待 External Task Client 獲取任務
執行中 任務執行中
出錯 執行時報錯
暫停 暫停流程例項執行
結束 正常流程結束
終止 人工關閉流程或者其他非正常結束關閉

自增 ID

一開始打算直接用 UUID 作為流程例項的 ID,但產品經理說使用跟原系統一樣的數字 ID 比較好,使用者用起來也比較習慣。

舉個例子,這個有點像 B 站以前用 AV 號而現在用 BV 號給使用者帶來的區別。

由於流程例項本身有許可權控制,用自增 ID 也爬不了多少。就算爬了也沒有什麼影響,所以還是保留了自增 ID,並且初始 ID 設定為比當前流程例項多一個數量級,以保證遷移過程中不會出現 ID 衝突。

當前所在的節點名稱

使用者在檢視列表的時候想直到流程例項的進度,可以用這個名稱展示給使用者。

另外還可以作為重啟流程例項時跳轉的節點。

建立來源

流程例項有時候碰到問題會回退給建立人,這就要求建立人必須是一個具體的使用者。而如果不標註來源,則該具體使用者可能無法獲取相關資訊來解決問題。

優先順序

這個欄位用於給 External Task 標註優先順序,優先順序越高越先執行。

處理人

分為兩種情況:

  • 填寫表單的人
  • 自動化步驟出現一些意外的問題,將會轉交給相關運維人員處理

流程例項狀態

對底層引擎流程例項狀態的快取,用於列表檢視時減少對底層引擎的查詢。

但是什麼時候更新這個狀態就成了一個問題。如果沒有及時更新,會給使用者帶來疑惑。比如說流程例項在底層引擎已經結束了,但是這個狀態卻不是結束狀態。後續會對此做詳細說明。

標籤

目前作為一些業務資訊的補充,僅用於展示。

流程節點日誌表

這個資訊用於展示哪些節點執行過,以及相關時間點和輸出資訊。

由於各種資料或者業務問題,或者確實是正常的執行但使用者誤認為流程例項的流向有問題,這個時候可以參考這些資訊。

欄位 作用
id 自增 ID,沒有額外作用
name 節點標識,用於確定唯一節點
i18n 翻譯標識,用於翻譯並展示節點名稱
pi_id 流程例項 ID,便於關聯和應對底層引擎 ID 的變化
status 執行狀態。等待執行、執行中、成功、失敗、超時
operator 操作人。可以是使用者,也可以是 External Task Client 的 ID
message 結果資訊
started_at 開始執行的時間
ended_at 結束執行的時間
timeout_at 超時的時間

節點標識

用於確定流程中的一個唯一節點,在繪製流程圖的時候配置。

由於同一個功能的節點在流程中可能被使用多次,因此該欄位會加上一些無關緊要的資訊來相互區分。

翻譯標識

由於同一個功能的節點在流程中可能被使用多次,在不同位置的同一個功能的節點所代表的業務含義不一定是相同的,所以翻譯標識要另外指定。

流程例項 ID

由於特殊業務需求,有些流程例項在被強行關閉或者正常結束後,需要重新啟用。

Camunda 提供了這種支援,但它建立了新的流程例項,其流程例項 ID 自然也就和之前的不同。

這時有兩種選擇:

  • 學 Camunda 將流程例項相關資訊複製一份,建立新例項
  • 保持當前例項不變,僅變更例項繫結的底層引擎例項

我選擇了第二種。因為第一種成本比較高,且在當前業務場景下沒有帶來價值,使用者也不願意接受。

超時時間

設定超時時間是為了避免由於各種不確定性原因導致沒有正常結束時,該節點狀態沒有得到更新,而使用者會覺得困惑。

服務定義表

該表用於管理節點和 API 的對映關係。

Camunda 有一種叫做 External Task 的節點型別,表示該任務是外部任務。需要外部系統主動拉取任務並提交結果。外部系統可以將這個拉取並提交結果的功能抽取出來,單獨建立一個 External Task Client (以下將其簡稱為 ETC) 。

ETC 從 Camunda 拉取特定 Topic 的 External Task,然後執行業務邏輯。

External Task 的 Topic 在配置流程圖的時候指定。ETC 在啟動時需要指定 Topic。

最開始寫 ETC 的時候,是參照官方的 JAVA 版本寫了一個 PHP 版本。

整個流程大致如下:

+-----------------------------------------+
|                                         |
| Adapter            (5)                  |
|                   Submit                |
|                                         |
|      +--------------------------+       |
|      |                          |       |
|      v             (1)          +       |   (3)
|              Fetch And Lock             | Request
| +----------+               +----------+ |         +--------+
| |          | <-----------+ | External | | +-----> | API    |
| |  Camunda |               | Task     | |         | System |
| |          | +-----------> | Client   | | <-----+ +--------+
| +----------+               +----------+ |
|              External Task              | Response
|                    (2)                  |   (4)
|                                         |
+-----------------------------------------+

使用這種方式時要解決的一個問題是:如何通過流程圖配置的節點資訊來判斷該節點執行時要請求哪個介面?

有一個簡單的做法是在節點的 Task ID 上應用一些規則。

這裡使用 “Task ID” 這個表述,是為了和 Camunda 保持一致。這個 Task ID 是一串由英文單片語成的有意義的字串。

例如將 Task ID 設定為:Users_Decisions_ShouldDoSomething

在 ETC 獲取該節點任務執行時,配置 ETC 將 ID 轉化為 POST /users/decisions/should-do-something

它的優點在於實現起來簡單,缺點是:

  • 難以控制請求方式。
    只能用 POST。不過如果要用其他的方法,可以將 Task ID 的第一個部分設定為方法,但 Task ID 會變得很長。如 Post_Users_Decisions_ShouldDoSomething
  • 難以做到近似的 Restful API 。
    比如實現 /users/{uid} 這種在 URL 上放使用者 ID 的功能,需要再對 Task ID 做定製。
  • 難以控制超時時間。
    超時時間是為了在各種問題導致 ETC 無法 complete 一個任務時,只要等待過了超時時間, ETC 就可以重新拉取到該任務。

超時時間是在第一步拉取任務的時候設定的,也就是在 ETC 上做配置。但是每個 ETC 只能配置一種超時時間。

最初的做法是每一種超時時間都設定一個特定 Topic,比如 XXX_3MIN 表示超時時間為 3 分鐘。然後啟動一些 ETC ,通過啟動引數配置對應的超時時間。但是從實踐的結果看,這樣更新起來不靈活,有時候還需要重啟 ETC。

上面這些問題雖然在 Task ID 上或者 Topic 上多做一些定製化就能完成,但是會使得它們自身變得越來越複雜。並且因為它們都是配置在流程圖上的,隨著 Task 越來越多,越難以更改。一旦要新加一個規則,會導致所有流程都得改一遍。

怎麼優化呢?

在 Adapter 層加一個 External Task 定義表。在 ETC 獲取到任務後查詢這個表,根據查詢結果做相應調整。

欄位 作用
id 自增 ID,沒有額外用途
task_id External Task ID
method HTTP 方法
url_path URL 的 Path 部分
url_query URL 的 Query 部分
timeout 超時時間

優化後的流程如下:

+-------------------------------------------+
|                                           |
| Adapter              (7)                  |
|                   Complete                |
|                                           |
|        +--------------------------+       |
|        |                          |       |
|        v             (1)          +       |   (5)
|                Fetch And Lock             | Request
|   +----------+               +----------+ |         +--------+
|   |          | <-----------+ | External | | +-----> | API    |
|   |  Camunda |               | Task     | |         | System |
|   |          | +-----------> | Client   | | <-----+ +--------+
|   +----------+               +----------+ |
|                External Task              | Response
|                      (2)        +    ^    |   (6)
|                                 |    |    |
|                      (3)        |    |    |
|                 Fetch API Info  |    |    |
| +------------+                  |    |    |
| | External   | <----------------+    |    |
| | Task       |                       |    |
| | Definition | +---------------------+    |
| +------------+                            |
|                    API Info               |
|                      (4)                  |
|                                           |
+-------------------------------------------+

服務請求客戶端管理表

這個表用於管理 ETC 客戶端。

主要解決兩個問題:

  • 關閉前等待執行中的任務結束
  • ETC 數量動態調整

關閉前等待執行中的任務結束

有時候要重啟 ETC,但是因為 ETC 總是會獲取任務執行,所以只能等深夜沒有流程在執行的時候重啟。

如果直接關閉的話,會導致任務雖然執行成功了,但由於沒有呼叫 Camunda 的 complete 而超時。超時就會重新執行。

如果是冪等的介面倒是不會出問題,但有些冪等難度大或者消耗的資源大,二次執行會出問題。

那麼讓 Camunda 在 ETC 請求任務的時候不給任務是否可行?因為獲取不到新任務後,總是能等到所有 ETC 執行中的任務都結束。

Camunda 提供了掛起(Suspend)流程例項的功能,雖然能避免流程例項的任務被 Fetch,但同時也使得正在執行的任務無法執行 complete。

那怎麼辦?

有兩種方式:

  • ETC 每次執行完一個任務後,就自動重啟
  • ETC 在向 Camunda 獲取任務前,都先查詢一下自己能否獲取任務

這裡選擇第二種,需要額外的表格維護 ETC 的資訊。

欄位 作用
id 自增 ID,沒有其他作用
etc_id 客戶端 ID
topic 客戶端獲取的任務的 topic
switch on/off 控制是否繼續獲取任務

ETC 數量動態調整

調整的依據來自於兩方面:

  • 流程例項的數量
  • 業務系統 API 的負載情況

如果流程例項的量大,且業務系統 API 負載比較低,可以新增更多 ETC ,加快整體的速度。

欄位 作用
id 自增 ID,沒有其他作用
topic 客戶端獲取的任務的 topic
count 啟動客戶端的數量

手動控制的話,這樣就夠了。如果要通過採集資訊自動控制,那麼可以再加兩個引數:

欄位 作用
count_max 啟動客戶端的最大數量
count_min 啟動客戶端的最小數量

動態表單定義表

Camunda 自身支援以下幾種型別的表單:

  • 嵌入式 HTML 表單
    對 Camunda 依賴性強。
  • 基於 XML 生成表單
    在流程圖繪製工具裡面定義表單,只能做簡單的功能。
  • JSF 表單
    和嵌入式 HTML 表單類似。
  • 通用表單
    功能太少。

由於業務上的表單比較複雜,又不能太過於依賴 Camunda,因此表單的定義和渲染需要另外做。

表單的功能至少需要包括:

  • 選擇項動態載入
  • 豐富的支援
    如上傳檔案和圖片展示。
  • 前端頁面資料格式校驗

表單有兩種形式:

  • 靜態表單
    把表單定義放在前端,前端直接渲染。
  • 動態表單
    把表單定義放在後端,前端提供基本元件。前端獲取後端對錶單的配置,根據這個配置做渲染。

我們選的是動態表單。一是因為原先的系統就是這麼做的,同時也比較靈活;二是因為我們團隊沒有專門的前端。

動態表單可以放業務層,也可以放流程引擎 Adapter 層。

如果想要多個接入流程引擎的系統都可以使用,可以放 Adapter 層。就算有的系統不想用這個動態表單,也完全不影響。流程引擎中臺的同事倒是對我們動態表單的實現比較感興趣。

分為兩張表:

  • 表單項元件表
  • 表單定義表

表單項元件表:

欄位 作用
id 自增 ID
name 元件名稱
config 元件的配置(Json),主要是資料校驗
default 預設值

前端用的是 Vue,每個表單項直接對應一個 Component 。

表單定義表:

欄位 作用
id 自增 ID
p_id 流程定義 ID
form_key 表單名
version 表單版本
group 表單內部分組,支援翻譯
cpn_id Component ID,表單項元件表中的 ID
order 在表單中所處的位置
field 變數名
label 渲染表單時,該元件的展示名稱,支援翻譯
config 元件的配置(Json),主要是資料校驗

表單暫存表

使用者在編輯完表單時,可能因為各種原因無法全部填寫完,又想儲存當前已填寫的資料。

可以建立一個資料表用於儲存這些暫存資料,當表單提交後刪除這些資料。

欄位 作用
id 自增 ID,沒有其他作用
pi_id 流程例項 ID
form_key 表單 ID
name 變數名稱
value 變數值,以 json 形式儲存

提交後的表單資料去哪了?

Camunda 裡的每個流程例項都可以有對應的流程例項變數集合,可以從下面的介面中獲取:

Get Process Variables
https://docs.camunda.org/manual/latest/reference/rest/process-instance/variables/get-variables/

為了便於理解,我畫了一張圖:

+-------------------------+
|                         |
| Process Instance        |
|                         |
| +----------+            |
| |          |            |
| | Global   +<-+         |
| | Variable |  |         |
| | Box      |  | Publish |
| |          |  |         |
| +--------+-+  |         |
|    fetch |    |         |
|          |    |         |
| +--------------------+  |
| |        |    |      |  |
| | Nodes  |    |      |  |
| |        |    |      |  |
| | +---------------+  |  |
| | |      |    |   |  |  |
| | | Node |    |   |  |  |
| | |      v    |   |  |  |
| | | +----+----++  |  |  |
| | | |          |  |  |  |
| | | | Local    |  |  |  |
| | | | Variable |  |  |  |
| | | | Box      |  |  |  |
| | | |          |  |  |  |
| | | +----------+  |  |  |
| | |               |  |  |
| | +---------------+  |  |
| |                    |  |
| +--------------------+  |
|                         |
+-------------------------+

即流程例項裡面會包含一個全域性的流程例項變數盒子,所有流程例項級別的變數都會放進去。

然後每個節點都有自己的本地變數盒子。它可以從全域性盒子獲取變數對映到本地盒子,也可以把本地變數釋出到全域性盒子。

流程例項的操作

  • 建立
  • 暫停和恢復
  • 節點跳轉
  • 表單處理
  • 關閉流程例項
  • 重啟流程例項

建立

先在自建流程例項表新增一條記錄,然後把流程例項的 ID 、建立人等一些基本資訊作為變數,呼叫流程引擎建立例項的介面時一起傳進去。

Start Process Instance
https://docs.camunda.org/manual/latest/reference/rest/process-definition/post-start-process-instance/

這些流程例項基本資訊的變數在儲存到 Camunda 裡面時,會給變數名加一個 meta 字首。

例如 id 加上字首後變成 meta__id

注意,如果變數名使用下劃線,在搜尋變數的時候不能用 GET 介面,要用 POST 介面。

Get Variable Instances
https://docs.camunda.org/manual/latest/reference/rest/variable-instance/get-query/
Get Variable Instances (POST)
https://docs.camunda.org/manual/latest/reference/rest/variable-instance/post-query/

表單處理

先建立流程例項再填表單還是反之?

兩種都可以。

我選擇的是先建立流程例項再填寫表單。

接下來分析兩者的優缺點。

先填寫表單再建立流程例項

優點:

  • 如果表單填寫一半時發現沒有必要走流程或者由於資料不足不能填完整,就不會建立流程例項

缺點:

  • 如果流程裡有其他表單,則初始表單與其他表單的處理邏輯不統一

先建立流程例項再填表單

優點:

  • 所有表單處理邏輯統一

缺點:

  • 必須建立流程例項才能填寫表單,如果最終不需要該流程例項,則流程例項列表會多出一個無用的例項

這個缺點可以緩解。

我見過一種實現:先建立例項,填完第一個表單後才在自己擴充套件的例項表中新增該例項。這樣使用者看流程例項列表就不會有無用的例項。

但是這個實現有個問題。使用者很有可能經常建立流程例項後不提交第一個表單,可能直接返回或者重新整理頁面丟失該資訊。經過一段時間會發現底層引擎保留大量流程例項,以至於流程引擎處理速度變慢。

獲取當前表單

由於 Camunda 做的是標準的流程引擎,因此介面中每個使用者都會有自己要處理的 UserTask(表單) 列表。

我們的場景是流程例項只會有一個節點執行,並且表單是和流程例項放在一起的。並且使用者要求要一次性檢視所有已填表單,包括其他人的表單。所以要從流程例項的角度處理。

我們需要一個 “獲取當前表單” 的介面,但由於上面的原因, Camunda 沒有現成的介面。只能自己根據 Camunda 已有介面封裝了。

  1. 獲取流程例項當前節點

    Get Activity Instance
    https://docs.camunda.org/manual/latest/reference/rest/process-instance/get-activity-instances/

  2. 根據節點 ID 獲取 Form Key

    Get Form Key
    https://docs.camunda.org/manual/latest/reference/rest/task/get-form-key/

獲取 Form Key 後就可以到動態表單定義表裡面獲取表單的定義,傳給前端渲染。

表單的暫存

前面提到表單提交後要刪除暫存的資料,因為如果沒有刪除這些資料,會碰到一個問題:

當使用者將流程例項駁回到前面的表單節點時,使用者修改表單但是不選擇提交而是暫存,下次使用者進入這個表單介面時資料是以 Camunda 裡面為準還是暫存的資料為準?

  • 如果選擇以暫存的資料為準,那麼要考慮一個場景:

    使用者初次提交表單後,流程例項後面的步驟修改了表單裡的某些資料。接著有使用者將流程例項駁回到這個表單,此時如果選擇以暫存資料為準,會導致表單展示的是未修改的資料,在業務上會出現問題。

  • 如果選擇以 Camunda 的資料為準,那麼就會導致使用者發現其修改並暫存的資料不見了

所以刪除暫存資料是一種解決方案,如果暫存表裡面有資料,就以暫存表為準,否則以 Camunda 為準。

表單資料校驗

資料校驗分為兩部分:

  • 資料型別校驗
  • 業務關係校驗

Camunda 自身支援資料型別校驗,但如果有複雜的型別就得在引擎層面自定義校驗類。

並且由於業務關係校驗不能放在引擎層面,所以兩者一起放在業務系統層面處理。

                  資料格式
 資料格式          業務關係
 校驗             校驗

+-------+        +--------+        +---------+
|       | submit |        | submit |         |
| front |        | system |        | engine  |
|  end  | +----> |        | +----> | adapter |
|       |        |        |        |         |
+-------+        +--------+        +---------+

暫存的介面一般只會執行資料格式的校驗,並且不對是否必填做校驗。

提交表單

提交表單到 Adapter 層的時候要做以下校驗:

  • 當前節點是否是表單節點
  • 當前表單節點的 Form Key 是否與提交的 Form Key 一致

流程例項轉交

轉交分為兩種型別:

  • 表單節點轉交
  • 自動化節點轉交
    指的是將操作權交給其他人,一般自動化節點出現錯誤的時候,會轉交給運維人員處理,運維人員可以轉給其他運維同事幫忙處理。

兩者都需要修改流程例項資訊表中的處理人欄位。

表單節點除此之外還要呼叫 Camunda 設定操作人的介面:

Set Assignee
https://docs.camunda.org/manual/latest/reference/rest/task/post-assignee/

暫停和恢復

Camunda 提供了一個 suspended 介面,用於掛起整個流程例項。使流程例項處於暫停狀態。

https://docs.camunda.org/manual/latest/reference/rest/process-instance/put-activate-suspend-by-id/

官方文件有關於掛起流程例項的完整說明。

https://docs.camunda.org/manual/latest/user-guide/process-engine/process-engine-concepts/#suspend-process-instances

一旦掛起流程例項,會產生以下影響:

  • 使用者無法提交表單
  • External Task Client (以下簡稱 ETC) 的 complete 無效
  • 使用者無法執行跳轉節點

雖然無法變更流程例項的執行節點,但是可以修改流程例項的全域性變數。

最初由於節點跳轉的需要,沒有把掛起直接作為流程例項的暫停功能,下面會對此做出解釋。

節點跳轉

節點跳轉最常用的就是駁回功能。之所以不直接說駁回,是因為除了駁回外,有時還需要跳轉到後面的節點。

這是因為自動化流程中,有一些節點會出現難以預測的問題。有的可以通過優化流程圖來解決,有的難以通過優化流程圖解決。所以需要人工干涉,跳過當前節點的執行或者返回前面的節點執行。

跳轉的介面

Camunda 對節點跳轉的支援是在流程例項修改介面。

https://docs.camunda.org/manual/latest/reference/rest/process-instance/post-modification/

它可以取消一個節點的執行(cancel),也可以開啟一個節點的執行(startBeforeActivity)。

執行修改介面時,有一個引數需要注意: skipIoMappings 。這個參數列示是否跳過節點輸入輸出對映的校驗。為了解釋這個引數,得先做個補充說明。

External Task 有個輸入輸出變數(Input/Output Variable)配置。用於將全域性變數對映到本地變數(Input),或者將本地變數釋出到全域性變數(Output)。

當開始執行一個 Task 之前,會對 Input Variable 執行對映。此時如果對映配置中的全域性變數不存在,就會報錯。因為變數不存在是一個錯誤的狀態,不能強行執行。結束一個 Task 則會對 Output Variable 執行對映。

如果一個 External Task 沒有執行完,就不會生成 Output Variable 所需的本地變數。這個時候如果取消該執行,會預設進入對映變數的邏輯,導致出錯。所以用 cancel 的時候需要開啟 skipIoMappings 。

而跳轉到目標節點時,又需要校驗 Input Variable 對映所需的全域性變數是否存在,否則強行執行會有問題。此時應該關閉 skipIoMappings 。

但 Camunda 這個 modification 介面的 skipIoMappings 放在最外層,表示一次只能設定一種 skipIoMappings 。

另外 skipCustomListeners 總是開啟。

因此想要實現跳轉,就得分為兩步:

  • 在目標節點 startBeforeActivity , 請求時關閉 skipIoMappings ,開啟 skipCustomListeners
  • 如果上一步成功,則在當前節點 cancel , 請求時開啟 skipIoMappings ,開啟 skipCustomListeners

需要注意一個問題:

執行跳轉的介面前要保證流程已處於暫停狀態。否則如果 cancel 節點時,節點已經完畢並轉入下一個節點,就會出現 cancel 失敗並且此時流程有兩個執行的節點。

但是如果用流程例項掛起介面使流程例項處於暫停狀態,也會受到掛起狀態的限制而沒辦法執行跳轉。

支援跳轉的暫停狀態

暫停的實現經歷過兩個版本。

最初版本中,節點跳轉前要求使用者必須先手動暫停流程例項。

前面提到掛起流程例項後無法跳轉節點,所以專門為當時的流程例項設定一個暫停的狀態。

如何實現可跳轉節點的暫停?

這裡要處理流程例項節點的兩種狀態:

  • 還未被 ETC 獲取。此時可以想辦法讓 ETC 沒辦法獲取到處於暫停狀態的流程例項的任務。
  • 已經被 ETC 獲取。此時可以讓 ETC 不執行 complete

接下來詳細說明。

首先是 還未被 ETC 獲取 的場景。

如何讓 ETC 不獲取暫停狀態的流程例項?

通過查詢文件得知流程例項碰到 failedExternalTask 這種 Incident 的時候, ETC 不會獲取該流程例項的任務。

https://docs.camunda.org/manual/latest/user-guide/process-engine/incidents/#incident-types

那麼如何生成這種 Incident ?

Incident 沒有一個 create 的介面,所以無法直接建立。

從剛才的文件上可以看到當 retries <= 0 的時候會生成 failedExternalTask 型別的 Incident 。

每個 External Task 的 retries 值預設為 1 。當 ETC 報告一個錯誤的時候,將 retries 減一。

但是使用者如果想要跳轉節點,不會想要等到當前節點出錯,萬一它不出錯怎麼辦?

通過找文件發現 External Task 有一個設定 retries 的介面。

https://docs.camunda.org/manual/latest/reference/rest/external-task/put-retries/

嘗試直接將 retries 設定為 0 ,發現可以生成 failedExternalTask 型別的 Incident 。

這樣節點還未被 ETC 獲取的場景就得到了處理。

接下來是 已經被 ETC 獲取 的場景。

在 ETC 獲取任務執行後設定 Incident 就沒法阻止,並且 Incident 的情況下 ETC 仍然可以 complete ,使得流程繼續往下走。所以如果只有上面的處理,點選暫停可能會出現失敗的情況。

這就得在 Adapter 層加一個處理。當 ETC 執行 complete 的時候請求給 Adapter,Adapter 查詢流程例項是否有 Incident,如果有就不提交給 Camunda。

但如果在查詢到沒 Incident 和提交給 Camunda 之間設定了 Incident 呢?

這個問題在於沒辦法通過介面請求對 Camunda 的表直接加鎖。

不過我們可以在自定義的流程例項資訊表裡的 status 上想辦法。

  • 在執行暫停的時候,將該流程例項所在行加排他鎖,然後更新為暫停狀態,更新完釋放鎖。
  • ETC 執行 complete 之前,對流程例項加排他鎖,查詢到的狀態如果是暫停狀態,則放棄 complete,否則執行 complete 。之後釋放鎖。

已經被 ETC 獲取的場景也處理了。

上述的暫停只能針對 External Task,其餘的就無法暫停了。

於是我後來重構了這部分程式碼,把 Incident 和 Suspend 結合在一起,讓跳轉的時候不需要先手動暫停。

步驟為:

  1. 設定 Suspend ,防止 ETC 獲取任務
  2. 流程例項資訊表設定狀態為 incident
  3. 設定流程引擎例項 Incident
  4. 取消 Suspend
  5. 設定新節點位置
  6. 取消當前節點,這個動作會連同 Incident 一起刪除

如何獲取跳轉獲取目標節點的 Task ID

有三種做法。

以下 “使用者” 表示運維人員或者某個使用者部門的公共賬號,不是所有人都有跳轉的許可權。

  • 通常的做法,即把執行過的節點以下拉選單的形式列出來,使用者選擇一個,然後執行。

    要實現這個功能,可以使用節點日誌的資訊。將節點執行日誌中的 Task ID 取出來去重。它的缺點是隻能跳轉到已經執行過的節點。

  • 將流程中所有節點列出來,讓使用者選擇。

    解決了第一種無法跳轉到未執行過的節點的問題。但帶來新的問題:流程所有節點的資訊如何獲取?

    Camunda 的介面沒有提供這個資訊,最多隻有流程圖的 xml 。解析 xml 是一種方法,不過也比較麻煩。

    如果在釋出流程定義的時候將所有節點資訊放入一張記錄節點資訊的表。這不僅需要解析 xml ,還需要新增一張資料表,更麻煩。

  • 直接將流程圖展示給使用者,使用者在流程圖上選擇一個節點,然後點選跳轉。

    不僅直觀,而且不用自己寫解析,直接用 Camunda 的 bpmn-js 。

    https://github.com/bpmn-io/bpmn-js

    bpmn-js 提供了很多示例。

    https://bpmn.io/toolkit/bpmn-js/examples/
    https://github.com/bpmn-io/bpmn-js-examples

    例如:

    • interaction: 與流程圖的互動,點選節點
    • overlays: 新增覆蓋層。可以在流程節點上加懸浮圖示來表示當前所在節點
    • colors: 給節點加顏色。比如將所有未執行過的節點設定為灰色,將執行過的節點設定為黑色。

我選擇第三種方式。

駁回功能

節點跳轉的另一個應用是駁回。駁回是流程例項當前具有控制權的使用者可以做的動作。

駁回通常有兩種場景:

  • 使用者選擇駁回到已經執行過的某個節點
    前端限制流程圖中只能選擇已執行過的節點,後端在跳轉前查詢執行日誌判斷該節點是否已執行過。
  • 所有流程例項只會駁回到第一個節點
    繪製流程圖的時候,為所有流程圖的開始節點設定相同的 ID

跳過當前節點

有的節點只執行一個操作,不生成任何對流程例項流轉有影響的資料。這種節點會因為各種奇怪的原因執行出錯,運維人員需要介入處理這些問題。處理完後跳過這些節點。

Camunda 有一個介面可以直接做到這件事:

https://docs.camunda.org/manual/latest/reference/rest/signal/post-signal/

相關說明文件:

https://docs.camunda.org/manual/latest/reference/bpmn20/events/signal-events/

最開始用過這個介面,不過後來取消了。還是讓運維人員選擇目標節點比較安全。

流程例項遷移(升級)

當流程釋出新版本之後,不會對已有的流程例項造成影響。如果想應用最新版本流程,則需要升級舊流程例項。

流程例項遷移分為三個步驟:

  1. 生成遷移計劃

    Generate Migration Plan
    https://docs.camunda.org/manual/latest/reference/rest/migration/generate-migration/

  2. 驗證遷移計劃

    Validate Migration Plan
    https://docs.camunda.org/manual/latest/reference/rest/migration/validate-migration-plan/

  3. 執行遷移計劃

    Execute Migration Plan
    https://docs.camunda.org/manual/latest/reference/rest/migration/execute-migration/

生成遷移計劃和驗證遷移計劃的時候,不會涉及具體的流程例項 ID。

執行遷移計劃的時候,可以選擇要遷移的具體流程例項 ID,也可以用查詢的方式指定要遷移的流程例項。

關閉流程例項

使用 Camunda 的 Delete 介面就行了。

Delete Process Instance
https://docs.camunda.org/manual/latest/reference/rest/process-instance/delete/

重啟流程例項

Camunda 會用舊流程例項的資訊來啟動一個新的流程例項。

Restart Process Instance
https://docs.camunda.org/manual/latest/reference/rest/process-definition/post-restart-process-instance-sync/

由於會建立一個新的流程例項,其 ID 與舊例項的 ID 不一致,因此得將流程例項資訊表中的引擎流程例項 ID 替換掉。

這裡會碰到一個問題: Camunda 重啟例項的介面不會返回新例項的 ID 。

還好我們之前在建立流程例項的時候,會往底層 Camunda 的全域性變數盒儲存自增 ID : meta__id

可以通過流程例項搜尋介面找到有儲存 meta__id 為指定 ID 的流程例項。

Get Instances (POST)
https://docs.camunda.org/manual/latest/reference/rest/process-instance/post-query/

然後將獲取到的流程例項 ID 更新到流程例項資訊表裡面。

前端表單如何渲染

寫一個主 Component,然後在裡面寫具體的各個元件。

遍歷後端傳的各元件名稱,建立多個主 Component。然後用元件名稱依次匹配裡面的各個元件,如果匹配到則展示。這裡用到了 Vue 的 v-if

結語

目前能想到的基本上都寫了。有一些細節的地方沒有深入討論,待後續繼續完善。

這篇沒有完全地按照當前專案寫的適配層的實踐來寫,而是在此基礎上做了一些優化。

相關文章