Skip to content

Commit 9db1b08

Browse files
committed
Add idempotency-key store with stored responses
RetryPolicy re-executes on retry and work_queue dedups only in-flight references — nothing cached the first result so a duplicate returned the same response without re-running the side effect. Add a Stripe-style IdempotencyStore (begin → new/in_progress/completed, complete, replay, fingerprint-conflict detection, injectable-clock TTL, JSON persistence) and request_fingerprint. Wired through facade, executor (AC_idempotency_ begin / AC_idempotency_complete, named-instance registry), MCP, and the Script Builder with a headless test batch and EN/Zh docs.
1 parent 8dbc7d3 commit 9db1b08

15 files changed

Lines changed: 390 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-22) — Idempotency-Key Store](#whats-new-2026-06-22--idempotency-key-store)
1617
- [What's new (2026-06-22) — Moving-Average Smoothing](#whats-new-2026-06-22--moving-average-smoothing)
1718
- [What's new (2026-06-22) — Single-Series Anomaly Detection](#whats-new-2026-06-22--single-series-anomaly-detection)
1819
- [What's new (2026-06-22) — Near-Duplicate Text Detection (SimHash / MinHash)](#whats-new-2026-06-22--near-duplicate-text-detection-simhash--minhash)
@@ -155,6 +156,12 @@
155156

156157
---
157158

159+
## What's new (2026-06-22) — Idempotency-Key Store
160+
161+
Run a side effect once, replay its response on retries. Full reference: [`docs/source/Eng/doc/new_features/v103_features_doc.rst`](docs/source/Eng/doc/new_features/v103_features_doc.rst).
162+
163+
- **`IdempotencyStore` / `request_fingerprint` / `IdempotencyConflict`** (`AC_idempotency_begin`, `AC_idempotency_complete`): `RetryPolicy` re-executes and `work_queue` dedups only in-flight refs — nothing cached the first result. This Stripe-style store returns `new`/`in_progress`/`completed` for a key, replays the stored response, raises on a fingerprint conflict, and supports injectable-clock TTL + JSON persistence. Pure-stdlib, deterministic.
164+
158165
## What's new (2026-06-22) — Moving-Average Smoothing
159166

160167
Smooth a noisy value series. Full reference: [`docs/source/Eng/doc/new_features/v102_features_doc.rst`](docs/source/Eng/doc/new_features/v102_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-22) — 幂等键存储](#本次更新-2026-06-22--幂等键存储)
1516
- [本次更新 (2026-06-22) — 移动平均平滑](#本次更新-2026-06-22--移动平均平滑)
1617
- [本次更新 (2026-06-22) — 单序列异常检测](#本次更新-2026-06-22--单序列异常检测)
1718
- [本次更新 (2026-06-22) — 近似重复文本检测(SimHash / MinHash)](#本次更新-2026-06-22--近似重复文本检测simhash--minhash)
@@ -158,6 +159,12 @@
158159

159160
平滑噪声值序列。完整参考:[`docs/source/Zh/doc/new_features/v102_features_doc.rst`](../docs/source/Zh/doc/new_features/v102_features_doc.rst)
160161

162+
## 本次更新 (2026-06-22) — 幂等键存储
163+
164+
副作用只执行一次,重试时重播其响应。完整参考:[`docs/source/Zh/doc/new_features/v103_features_doc.rst`](../docs/source/Zh/doc/new_features/v103_features_doc.rst)
165+
166+
- **`IdempotencyStore` / `request_fingerprint` / `IdempotencyConflict`**(`AC_idempotency_begin``AC_idempotency_complete`):`RetryPolicy` 重试会重跑,`work_queue` 只对进行中引用去重 —— 没有东西缓存第一次结果。本 Stripe 风格存储为某键返回 `new`/`in_progress`/`completed`、重播已存储响应、指纹冲突时抛出异常,并支持可注入时钟 TTL + JSON 持久化。纯标准库、确定。
167+
161168
- **`sma` / `wma` / `ewma` / `rolling`**(`AC_sma``AC_ewma`):`stats.describe` 汇总整个样本,`timeseries` 把计数器滚成速率,但没有东西能平滑噪声信号。本功能加入尾端简单/加权/指数加权移动平均与通用滚动归约器,全部返回与输入时间线对齐的等长 list。纯标准库、确定。
162169

163170
## 本次更新 (2026-06-22) — 单序列异常检测

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-22) — 冪等鍵儲存](#本次更新-2026-06-22--冪等鍵儲存)
1516
- [本次更新 (2026-06-22) — 移動平均平滑](#本次更新-2026-06-22--移動平均平滑)
1617
- [本次更新 (2026-06-22) — 單序列異常偵測](#本次更新-2026-06-22--單序列異常偵測)
1718
- [本次更新 (2026-06-22) — 近似重複文字偵測(SimHash / MinHash)](#本次更新-2026-06-22--近似重複文字偵測simhash--minhash)
@@ -158,6 +159,12 @@
158159

159160
平滑雜訊值序列。完整參考:[`docs/source/Zh/doc/new_features/v102_features_doc.rst`](../docs/source/Zh/doc/new_features/v102_features_doc.rst)
160161

162+
## 本次更新 (2026-06-22) — 冪等鍵儲存
163+
164+
副作用只執行一次,重試時重播其回應。完整參考:[`docs/source/Zh/doc/new_features/v103_features_doc.rst`](../docs/source/Zh/doc/new_features/v103_features_doc.rst)
165+
166+
- **`IdempotencyStore` / `request_fingerprint` / `IdempotencyConflict`**(`AC_idempotency_begin``AC_idempotency_complete`):`RetryPolicy` 重試會重跑,`work_queue` 只對進行中參照去重 —— 沒有東西快取第一次結果。本 Stripe 風格儲存為某鍵回傳 `new`/`in_progress`/`completed`、重播已儲存回應、指紋衝突時拋出例外,並支援可注入時鐘 TTL + JSON 持久化。純標準函式庫、具決定性。
167+
161168
- **`sma` / `wma` / `ewma` / `rolling`**(`AC_sma``AC_ewma`):`stats.describe` 彙總整個樣本,`timeseries` 把計數器滾成速率,但沒有東西能平滑雜訊訊號。本功能加入尾端簡單/加權/指數加權移動平均與通用滾動歸約器,全部回傳與輸入時間線對齊的等長 list。純標準函式庫、具決定性。
162169

163170
## 本次更新 (2026-06-22) — 單序列異常偵測
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Idempotency-Key Store
2+
=====================
3+
4+
``resilience.RetryPolicy`` *re-executes* on retry and ``work_queue`` dedups only
5+
in-flight references — nothing cached the first result so a duplicate request
6+
returns the *same* response without re-running the side effect. This is the
7+
Stripe idempotency pattern: register a key, run the work once, and replay the
8+
stored response for any duplicate.
9+
10+
Pure standard library (``hashlib`` / ``json``); imports no ``PySide6``. The
11+
clock is injectable and the store is in-memory with JSON persistence, so TTL
12+
expiry and replay are fully deterministic in CI.
13+
14+
Headless API
15+
------------
16+
17+
.. code-block:: python
18+
19+
from je_auto_control import IdempotencyStore, request_fingerprint
20+
21+
store = IdempotencyStore(ttl=86400)
22+
state = store.begin("order-42", request_fingerprint(payload))
23+
if state["status"] == "completed":
24+
return state["response"] # replay — do not re-run
25+
result = charge(payload) # run the side effect once
26+
store.complete("order-42", result)
27+
28+
``begin`` returns ``{status, response}`` where status is ``new`` (first time),
29+
``in_progress`` (a duplicate before completion), or ``completed`` (replay the
30+
stored response); reusing a key with a different ``request`` fingerprint raises
31+
``IdempotencyConflict`` (Stripe's HTTP-400 behaviour). ``complete`` records the
32+
response, ``get`` reads a live record, and ``save`` / ``load`` persist the store
33+
as JSON. ``request_fingerprint`` is a stable, order-independent SHA-256 of a
34+
payload.
35+
36+
Executor commands
37+
-----------------
38+
39+
``AC_idempotency_begin`` registers/looks up a ``key`` in a named store (optional
40+
``request`` for conflict detection); ``AC_idempotency_complete`` stores the
41+
``response``. Both use a named-instance registry (like circuit breakers /
42+
bulkheads) and are exposed as MCP tools (``ac_idempotency_begin`` /
43+
``ac_idempotency_complete``) and as Script Builder commands under **Flow**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ Comprehensive guides for all AutoControl features.
125125
doc/new_features/v100_features_doc
126126
doc/new_features/v101_features_doc
127127
doc/new_features/v102_features_doc
128+
doc/new_features/v103_features_doc
128129
doc/ocr_backends/ocr_backends_doc
129130
doc/observability/observability_doc
130131
doc/operations_layer/operations_layer_doc
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
冪等鍵儲存
2+
========
3+
4+
``resilience.RetryPolicy`` 在重試時*重新執行*,``work_queue`` 只對進行中的參照去重 —— 沒有東西快取
5+
第一次的結果,讓重複請求能回傳*相同*回應而不重跑副作用。這是 Stripe 的冪等模式:註冊一個鍵、把工作
6+
執行一次,並為任何重複請求重播已儲存的回應。
7+
8+
純標準函式庫(``hashlib`` / ``json``);不匯入 ``PySide6``。時鐘可注入,儲存為記憶體內並具 JSON 持久化,
9+
因此 TTL 過期與重播在 CI 中完全具決定性。
10+
11+
無頭 API
12+
--------
13+
14+
.. code-block:: python
15+
16+
from je_auto_control import IdempotencyStore, request_fingerprint
17+
18+
store = IdempotencyStore(ttl=86400)
19+
state = store.begin("order-42", request_fingerprint(payload))
20+
if state["status"] == "completed":
21+
return state["response"] # 重播 —— 不要重跑
22+
result = charge(payload) # 副作用只執行一次
23+
store.complete("order-42", result)
24+
25+
``begin`` 回傳 ``{status, response}``,status 為 ``new``(首次)、``in_progress``(完成前的重複)或
26+
``completed``(重播已儲存回應);以不同 ``request`` 指紋重用同一鍵會拋出 ``IdempotencyConflict``
27+
(Stripe 的 HTTP-400 行為)。``complete`` 記錄回應,``get`` 讀取有效記錄,``save`` / ``load`` 以 JSON
28+
持久化。``request_fingerprint`` 是 payload 的穩定、與順序無關的 SHA-256。
29+
30+
執行器命令
31+
----------
32+
33+
``AC_idempotency_begin`` 在具名儲存中註冊/查找 ``key``(可選 ``request`` 做衝突偵測);
34+
``AC_idempotency_complete`` 儲存 ``response``。兩者使用具名實例登錄(如斷路器/隔艙),並以 MCP 工具
35+
(``ac_idempotency_begin`` / ``ac_idempotency_complete``)以及 Script Builder 中 **Flow** 分類下的命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ AutoControl 所有功能的完整使用指南。
125125
doc/new_features/v100_features_doc
126126
doc/new_features/v101_features_doc
127127
doc/new_features/v102_features_doc
128+
doc/new_features/v103_features_doc
128129
doc/ocr_backends/ocr_backends_doc
129130
doc/observability/observability_doc
130131
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@
199199
from je_auto_control.utils.resilience import (
200200
CircuitBreaker, CircuitOpenError, RetryPolicy, retry_call,
201201
)
202+
# Idempotency-key store with stored responses (Stripe-style request dedup)
203+
from je_auto_control.utils.idempotency import (
204+
IdempotencyConflict, IdempotencyStore, request_fingerprint,
205+
)
202206
# CI workflow annotations (GitHub Actions)
203207
from je_auto_control.utils.ci_annotations import (
204208
emit_annotations, format_annotation,
@@ -925,6 +929,7 @@ def start_autocontrol_gui(*args, **kwargs):
925929
"snapshot_screen",
926930
"replay_timeline", "run_sequence",
927931
"CircuitBreaker", "CircuitOpenError", "RetryPolicy", "retry_call",
932+
"IdempotencyConflict", "IdempotencyStore", "request_fingerprint",
928933
"emit_annotations", "format_annotation",
929934
"ClipboardHistory", "default_clipboard_history",
930935
"analyze_heal_log", "heal_stats", "scan_secrets",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,6 +1995,25 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
19951995
),
19961996
description="Exponentially-weighted moving average of a series.",
19971997
))
1998+
specs.append(CommandSpec(
1999+
"AC_idempotency_begin", "Flow", "Idempotency: Begin",
2000+
fields=(
2001+
FieldSpec("name", FieldType.STRING, placeholder="payments"),
2002+
FieldSpec("key", FieldType.STRING, placeholder="order-42"),
2003+
FieldSpec("request", FieldType.STRING, optional=True,
2004+
placeholder='{"amount": 100}'),
2005+
),
2006+
description="Register/look up an idempotency key (new/in_progress/done).",
2007+
))
2008+
specs.append(CommandSpec(
2009+
"AC_idempotency_complete", "Flow", "Idempotency: Complete",
2010+
fields=(
2011+
FieldSpec("name", FieldType.STRING, placeholder="payments"),
2012+
FieldSpec("key", FieldType.STRING, placeholder="order-42"),
2013+
FieldSpec("response", FieldType.STRING, placeholder='{"ok": true}'),
2014+
),
2015+
description="Store the completed response for an idempotency key.",
2016+
))
19982017
specs.append(CommandSpec(
19992018
"AC_diff_rows", "Data", "Dataset Diff: Rows by Key",
20002019
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2929,6 +2929,26 @@ def _rate_limit(name: str, rate: float = 1.0, capacity: float = 1.0,
29292929

29302930

29312931
_BULKHEADS: Dict[str, Any] = {}
2932+
_IDEMPOTENCY_STORES: Dict[str, Any] = {}
2933+
2934+
2935+
def _idempotency_begin(name: str, key: str,
2936+
request: Any = None) -> Dict[str, Any]:
2937+
"""Adapter: register/look up an idempotency key in a named store."""
2938+
from je_auto_control.utils.idempotency import (
2939+
IdempotencyStore, request_fingerprint)
2940+
store = _IDEMPOTENCY_STORES.setdefault(name, IdempotencyStore())
2941+
fingerprint = request_fingerprint(request) if request is not None else None
2942+
return store.begin(key, fingerprint)
2943+
2944+
2945+
def _idempotency_complete(name: str, key: str,
2946+
response: Any) -> Dict[str, Any]:
2947+
"""Adapter: store the completed response for an idempotency key."""
2948+
from je_auto_control.utils.idempotency import IdempotencyStore
2949+
store = _IDEMPOTENCY_STORES.setdefault(name, IdempotencyStore())
2950+
store.complete(key, response)
2951+
return {"status": "completed"}
29322952

29332953

29342954
def _bulkhead_run(name: str, max_concurrent: int,
@@ -4544,6 +4564,8 @@ def __init__(self):
45444564
"AC_detect_anomalies": _detect_anomalies,
45454565
"AC_sma": _sma,
45464566
"AC_ewma": _ewma,
4567+
"AC_idempotency_begin": _idempotency_begin,
4568+
"AC_idempotency_complete": _idempotency_complete,
45474569
"AC_detect_drift": _detect_drift,
45484570
"AC_categorical_drift": _categorical_drift,
45494571
"AC_diff_rows": _diff_rows,

0 commit comments

Comments
 (0)