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

上一篇說明了選擇 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

結語

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章