Skip to content

Commit 22c6841

Browse files
committed
Add saga orchestrator with compensating rollback
1 parent 5e54e60 commit 22c6841

15 files changed

Lines changed: 363 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
## Table of Contents
1515

16+
- [What's new (2026-06-20) — Saga / Compensating Rollback](#whats-new-2026-06-20--saga--compensating-rollback)
1617
- [What's new (2026-06-20) — JSONPath Querying](#whats-new-2026-06-20--jsonpath-querying)
1718
- [What's new (2026-06-20) — Multi-Channel Webhook Notifications](#whats-new-2026-06-20--multi-channel-webhook-notifications)
1819
- [What's new (2026-06-20) — Outbound CloudEvents Emitter](#whats-new-2026-06-20--outbound-cloudevents-emitter)
@@ -104,6 +105,12 @@
104105

105106
---
106107

108+
## What's new (2026-06-20) — Saga / Compensating Rollback
109+
110+
Undo completed steps when a later one fails. Full reference: [`docs/source/Eng/doc/new_features/v52_features_doc.rst`](docs/source/Eng/doc/new_features/v52_features_doc.rst).
111+
112+
- **`Saga` / `run_saga`** (`AC_run_saga`, `ac_run_saga`): records a compensating action per step; on any failure runs the completed steps' compensations in **LIFO** order — the durable-transaction primitive `AC_try` (single-block) couldn't provide. Forward actions/compensations are callables (or JSON action lists), so it's fully unit-tested with no side effects; compensation is best-effort (a failing undo is logged, rollback continues). Returns `{ok, completed, compensated, failed_step, error}`.
113+
107114
## What's new (2026-06-20) — JSONPath Querying
108115

109116
Query API/DB JSON with wildcards, recursion, filters. Full reference: [`docs/source/Eng/doc/new_features/v51_features_doc.rst`](docs/source/Eng/doc/new_features/v51_features_doc.rst).

README/README_zh-CN.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目录
1414

15+
- [本次更新 (2026-06-20) — Saga / 补偿回滚](#本次更新-2026-06-20--saga--补偿回滚)
1516
- [本次更新 (2026-06-20) — JSONPath 查询](#本次更新-2026-06-20--jsonpath-查询)
1617
- [本次更新 (2026-06-20) — 多通道 Webhook 通知](#本次更新-2026-06-20--多通道-webhook-通知)
1718
- [本次更新 (2026-06-20) — 对外 CloudEvents 发送器](#本次更新-2026-06-20--对外-cloudevents-发送器)
@@ -103,6 +104,12 @@
103104

104105
---
105106

107+
## 本次更新 (2026-06-20) — Saga / 补偿回滚
108+
109+
后续步骤失败时回滚已完成步骤。完整参考:[`docs/source/Zh/doc/new_features/v52_features_doc.rst`](../docs/source/Zh/doc/new_features/v52_features_doc.rst)
110+
111+
- **`Saga` / `run_saga`**(`AC_run_saga``ac_run_saga`):为每个步骤记录补偿动作;任何失败时以 **LIFO** 顺序对已完成步骤执行补偿 —— 单一区块的 `AC_try` 无法提供的持久性事务原语。前向动作/补偿为可调用对象(或 JSON 动作列表),因此可在无副作用下完整单元测试;补偿为尽力而为(失败的回滚会记录,回滚继续)。返回 `{ok, completed, compensated, failed_step, error}`
112+
106113
## 本次更新 (2026-06-20) — JSONPath 查询
107114

108115
以通配符、递归、过滤查询 API/DB JSON。完整参考:[`docs/source/Zh/doc/new_features/v51_features_doc.rst`](../docs/source/Zh/doc/new_features/v51_features_doc.rst)

README/README_zh-TW.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目錄
1414

15+
- [本次更新 (2026-06-20) — Saga / 補償回溯](#本次更新-2026-06-20--saga--補償回溯)
1516
- [本次更新 (2026-06-20) — JSONPath 查詢](#本次更新-2026-06-20--jsonpath-查詢)
1617
- [本次更新 (2026-06-20) — 多通道 Webhook 通知](#本次更新-2026-06-20--多通道-webhook-通知)
1718
- [本次更新 (2026-06-20) — 對外 CloudEvents 發送器](#本次更新-2026-06-20--對外-cloudevents-發送器)
@@ -103,6 +104,12 @@
103104

104105
---
105106

107+
## 本次更新 (2026-06-20) — Saga / 補償回溯
108+
109+
後續步驟失敗時復原已完成步驟。完整參考:[`docs/source/Zh/doc/new_features/v52_features_doc.rst`](../docs/source/Zh/doc/new_features/v52_features_doc.rst)
110+
111+
- **`Saga` / `run_saga`**(`AC_run_saga``ac_run_saga`):為每個步驟記錄補償動作;任何失敗時以 **LIFO** 順序對已完成步驟執行補償 —— 單一區塊的 `AC_try` 無法提供的持久性交易原語。前向動作/補償為可呼叫物件(或 JSON 動作清單),因此可在無副作用下完整單元測試;補償為盡力而為(失敗的復原會記錄,回溯繼續)。回傳 `{ok, completed, compensated, failed_step, error}`
112+
106113
## 本次更新 (2026-06-20) — JSONPath 查詢
107114

108115
以萬用字元、遞迴、過濾查詢 API/DB JSON。完整參考:[`docs/source/Zh/doc/new_features/v51_features_doc.rst`](../docs/source/Zh/doc/new_features/v51_features_doc.rst)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
Saga / Compensating Rollback
2+
============================
3+
4+
Some automations span several irreversible-looking steps — create a record, send
5+
an email, move a file. If a later step fails, the already-completed steps should
6+
be **undone**, but the executor's ``AC_try`` only does try/catch/finally for one
7+
block; nothing tracked "what to undo" across N completed steps. A ``Saga``
8+
records a compensating action per step and, on any failure, runs the
9+
compensations for the completed steps in **LIFO** order.
10+
11+
Forward actions and compensations are plain callables (or, via the executor, JSON
12+
action lists), so the orchestration is fully unit-testable with no real side
13+
effects. Compensation is best-effort: a failing compensation is logged and the
14+
rollback continues. Imports no ``PySide6``.
15+
16+
Headless API
17+
------------
18+
19+
.. code-block:: python
20+
21+
from je_auto_control import Saga
22+
23+
result = (Saga()
24+
.step("create", create_record, delete_record)
25+
.step("notify", send_email, None) # nothing to undo
26+
.step("move", move_file, restore_file)
27+
.run())
28+
29+
if not result.ok:
30+
result.failed_step # which step raised
31+
result.completed # steps that ran forward
32+
result.compensated # steps undone (LIFO over completed)
33+
34+
``run()`` returns a ``SagaResult`` (``ok`` / ``completed`` / ``compensated`` /
35+
``failed_step`` / ``error``). A step "fails" when its action raises; steps with no
36+
compensation are simply skipped during rollback.
37+
38+
Executor command
39+
----------------
40+
41+
``AC_run_saga`` takes ``steps`` — a list (or JSON string) of ``{name, action:
42+
[...], compensation: [...]}`` where each ``action`` / ``compensation`` is an
43+
AutoControl action list. It returns ``{ok, completed, compensated, failed_step,
44+
error}``. The same operation is exposed as the MCP tool ``ac_run_saga`` and as a
45+
Script Builder command under **Flow**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Comprehensive guides for all AutoControl features.
7474
doc/new_features/v49_features_doc
7575
doc/new_features/v50_features_doc
7676
doc/new_features/v51_features_doc
77+
doc/new_features/v52_features_doc
7778
doc/ocr_backends/ocr_backends_doc
7879
doc/observability/observability_doc
7980
doc/operations_layer/operations_layer_doc
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
Saga / 補償回溯
2+
===============
3+
4+
有些自動化橫跨數個看似不可逆的步驟 —— 建立紀錄、寄送郵件、移動檔案。若後續步驟失敗,
5+
已完成的步驟應被**復原**,但執行器的 ``AC_try`` 只對單一區塊做 try/catch/finally;沒有
6+
任何機制追蹤跨 N 個已完成步驟「該復原什麼」。``Saga`` 為每個步驟記錄一個補償動作,並在
7+
任何失敗時以 **LIFO** 順序對已完成步驟執行補償。
8+
9+
前向動作與補償皆為純可呼叫物件(或經執行器以 JSON 動作清單),因此編排可在無任何真實副
10+
作用下完整單元測試。補償為盡力而為:失敗的補償會被記錄,回溯繼續進行。不匯入
11+
``PySide6``。
12+
13+
無頭 API
14+
--------
15+
16+
.. code-block:: python
17+
18+
from je_auto_control import Saga
19+
20+
result = (Saga()
21+
.step("create", create_record, delete_record)
22+
.step("notify", send_email, None) # 無需復原
23+
.step("move", move_file, restore_file)
24+
.run())
25+
26+
if not result.ok:
27+
result.failed_step # 哪個步驟拋出例外
28+
result.completed # 前向執行過的步驟
29+
result.compensated # 已復原的步驟(對已完成者 LIFO)
30+
31+
``run()`` 回傳 ``SagaResult``(``ok`` / ``completed`` / ``compensated`` /
32+
``failed_step`` / ``error``)。步驟「失敗」即其動作拋出例外;沒有補償的步驟在回溯時直接略
33+
過。
34+
35+
執行器指令
36+
----------
37+
38+
``AC_run_saga`` 接受 ``steps`` —— 一個 ``{name, action: [...], compensation: [...]}``
39+
的清單(或 JSON 字串),其中 ``action`` / ``compensation`` 各為一個 AutoControl 動作清單。
40+
回傳 ``{ok, completed, compensated, failed_step, error}``。相同操作亦提供為 MCP 工具
41+
``ac_run_saga``,以及 Script Builder 中 **Flow** 分類下的指令。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ AutoControl 所有功能的完整使用指南。
7474
doc/new_features/v49_features_doc
7575
doc/new_features/v50_features_doc
7676
doc/new_features/v51_features_doc
77+
doc/new_features/v52_features_doc
7778
doc/ocr_backends/ocr_backends_doc
7879
doc/observability/observability_doc
7980
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@
280280
from je_auto_control.utils.jsonpath import (
281281
json_extract, json_query, json_query_one,
282282
)
283+
# Saga orchestrator: multi-step flow with compensating rollback
284+
from je_auto_control.utils.saga import Saga, SagaResult, run_saga
283285
# Background popup/interrupt watchdog (unattended automation)
284286
from je_auto_control.utils.watchdog import (
285287
PopupWatchdog, WatchdogRule, default_popup_watchdog,
@@ -743,6 +745,7 @@ def start_autocontrol_gui(*args, **kwargs):
743745
"EventEmitter", "post_cloudevent", "to_cloudevent",
744746
"WebhookChannel", "WebhookResult", "notify_webhook", "set_default_poster",
745747
"json_extract", "json_query", "json_query_one",
748+
"Saga", "SagaResult", "run_saga",
746749
# MCP server
747750
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
748751
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,15 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
11451145
),
11461146
description="Extract a {key: jsonpath} mapping into a flat object.",
11471147
))
1148+
specs.append(CommandSpec(
1149+
"AC_run_saga", "Flow", "Run Saga (Compensating Rollback)",
1150+
fields=(
1151+
FieldSpec("steps", FieldType.STRING,
1152+
placeholder='[{"name": "s1", "action": [...], '
1153+
'"compensation": [...]}]'),
1154+
),
1155+
description="Run steps; on failure undo completed steps LIFO.",
1156+
))
11481157
specs.append(CommandSpec(
11491158
"AC_generate_sop", "Report", "Generate SOP Document",
11501159
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3316,6 +3316,18 @@ def _json_extract(data: Any, mapping: Any) -> Dict[str, Any]:
33163316
return {"result": json_extract(data, mapping)}
33173317

33183318

3319+
def _run_saga(steps: Any) -> Dict[str, Any]:
3320+
"""Adapter: run a saga (steps with compensating rollback) from a spec."""
3321+
import json
3322+
from je_auto_control.utils.saga import run_saga
3323+
if isinstance(steps, str):
3324+
steps = json.loads(steps)
3325+
result = run_saga(steps)
3326+
return {"ok": result.ok, "completed": result.completed,
3327+
"compensated": result.compensated,
3328+
"failed_step": result.failed_step, "error": result.error}
3329+
3330+
33193331
class Executor:
33203332
"""
33213333
Executor
@@ -3598,6 +3610,7 @@ def __init__(self):
35983610
"AC_notify_webhook": _notify_webhook,
35993611
"AC_json_query": _json_query,
36003612
"AC_json_extract": _json_extract,
3613+
"AC_run_saga": _run_saga,
36013614
"AC_a11y_record_start": _a11y_record_start,
36023615
"AC_a11y_record_stop": _a11y_record_stop,
36033616
"AC_a11y_record_events": _a11y_record_events,

0 commit comments

Comments
 (0)