Skip to content

Commit dbc24a7

Browse files
authored
Merge pull request #314 from Integration-Automation/feat/optimistic-batch
Add optimistic-concurrency versioned store (CAS / If-Match)
2 parents d41b970 + 95cc8c6 commit dbc24a7

15 files changed

Lines changed: 387 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) — Optimistic-Concurrency Versioned Store](#whats-new-2026-06-22--optimistic-concurrency-versioned-store)
1617
- [What's new (2026-06-22) — Per-Stream Sequence-Gap Detection](#whats-new-2026-06-22--per-stream-sequence-gap-detection)
1718
- [What's new (2026-06-22) — Time-Windowed Deduplication](#whats-new-2026-06-22--time-windowed-deduplication)
1819
- [What's new (2026-06-22) — Idempotency-Key Store](#whats-new-2026-06-22--idempotency-key-store)
@@ -158,6 +159,12 @@
158159

159160
---
160161

162+
## What's new (2026-06-22) — Optimistic-Concurrency Versioned Store
163+
164+
Update only if the version is unchanged (compare-and-swap / If-Match). Full reference: [`docs/source/Eng/doc/new_features/v106_features_doc.rst`](docs/source/Eng/doc/new_features/v106_features_doc.rst).
165+
166+
- **`VersionedStore` / `VersionConflict` / `if_match_header` / `check_if_match`** (`AC_cas_put`, `AC_cas_get`): `http_conditional` used ETag for read caching but never for write concurrency. This local compare-and-swap store `put`s only when `expected_version` matches (raising `VersionConflict` on a stale write), bumps a monotonic version, and bridges to HTTP `If-Match` — the write side of the ETag story. Pure-stdlib, deterministic.
167+
161168
## What's new (2026-06-22) — Per-Stream Sequence-Gap Detection
162169

163170
Detect missing / out-of-order / duplicate messages by sequence number. Full reference: [`docs/source/Eng/doc/new_features/v105_features_doc.rst`](docs/source/Eng/doc/new_features/v105_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) — 幂等键存储](#本次更新-2026-06-22--幂等键存储)
@@ -161,6 +162,12 @@
161162

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

165+
## 本次更新 (2026-06-22) — 乐观并发版本存储
166+
167+
只在版本未变时更新(compare-and-swap / If-Match)。完整参考:[`docs/source/Zh/doc/new_features/v106_features_doc.rst`](../docs/source/Zh/doc/new_features/v106_features_doc.rst)
168+
169+
- **`VersionedStore` / `VersionConflict` / `if_match_header` / `check_if_match`**(`AC_cas_put``AC_cas_get`):`http_conditional` 以 ETag 做读取缓存,但从不用于写入并发。本地 compare-and-swap 存储仅在 `expected_version` 相符时 `put`(过时写入抛出 `VersionConflict`)、递增单调版本,并桥接到 HTTP `If-Match` —— ETag 故事的写入面。纯标准库、确定。
170+
164171
## 本次更新 (2026-06-22) — 逐流序号间隙检测
165172

166173
按序号检测遗漏/乱序/重复的消息。完整参考:[`docs/source/Zh/doc/new_features/v105_features_doc.rst`](../docs/source/Zh/doc/new_features/v105_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-22) — 樂觀並行版本儲存](#本次更新-2026-06-22--樂觀並行版本儲存)
1516
- [本次更新 (2026-06-22) — 逐串流序號間隙偵測](#本次更新-2026-06-22--逐串流序號間隙偵測)
1617
- [本次更新 (2026-06-22) — 時間視窗去重](#本次更新-2026-06-22--時間視窗去重)
1718
- [本次更新 (2026-06-22) — 冪等鍵儲存](#本次更新-2026-06-22--冪等鍵儲存)
@@ -161,6 +162,12 @@
161162

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

165+
## 本次更新 (2026-06-22) — 樂觀並行版本儲存
166+
167+
只在版本未變時更新(compare-and-swap / If-Match)。完整參考:[`docs/source/Zh/doc/new_features/v106_features_doc.rst`](../docs/source/Zh/doc/new_features/v106_features_doc.rst)
168+
169+
- **`VersionedStore` / `VersionConflict` / `if_match_header` / `check_if_match`**(`AC_cas_put``AC_cas_get`):`http_conditional` 以 ETag 做讀取快取,但從不用於寫入並行。本地 compare-and-swap 儲存僅在 `expected_version` 相符時 `put`(過時寫入拋出 `VersionConflict`)、遞增單調版本,並橋接到 HTTP `If-Match` —— ETag 故事的寫入面。純標準函式庫、具決定性。
170+
164171
## 本次更新 (2026-06-22) — 逐串流序號間隙偵測
165172

166173
依序號偵測遺漏/亂序/重複的訊息。完整參考:[`docs/source/Zh/doc/new_features/v105_features_doc.rst`](../docs/source/Zh/doc/new_features/v105_features_doc.rst)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
Optimistic-Concurrency Versioned Store
2+
======================================
3+
4+
``http_conditional`` uses ETag for *read* caching (``If-None-Match`` / 304) but
5+
never for *write* concurrency (``If-Match`` / version check). There was no local
6+
compare-and-swap / versioned record store for "update only if the version is
7+
unchanged". This fills the write side of the ETag story.
8+
9+
Pure standard library (``json``); imports no ``PySide6``. The version is a
10+
monotonic int and the store is in-memory with JSON persistence, so behaviour is
11+
fully deterministic in CI.
12+
13+
Headless API
14+
------------
15+
16+
.. code-block:: python
17+
18+
from je_auto_control import VersionedStore, VersionConflict, if_match_header
19+
20+
store = VersionedStore()
21+
version = store.put("db.host", "prod-1") # version 1
22+
record = store.get("db.host") # {"value": ..., "version": 1}
23+
try:
24+
store.put("db.host", "prod-2", expected_version=record["version"])
25+
except VersionConflict:
26+
reload_and_retry()
27+
header = if_match_header(version) # '"1"' for an HTTP If-Match
28+
29+
``put`` writes only when ``expected_version`` matches the current version
30+
(``0`` requires the key to be absent, omitting it is a blind write) and returns
31+
the new version, raising ``VersionConflict`` on a stale write. ``get`` returns
32+
``{value, version}``; ``delete`` is likewise guarded; ``save`` / ``load``
33+
persist as JSON. ``if_match_header`` / ``check_if_match`` bridge to real HTTP
34+
``If-Match`` writes alongside ``http_conditional``.
35+
36+
Executor commands
37+
-----------------
38+
39+
``AC_cas_put`` returns ``{ok, version}`` (or ``{ok: false, error}`` on conflict);
40+
``AC_cas_get`` returns ``{record}``. Both use a named-instance registry and are
41+
exposed as MCP tools (``ac_cas_put`` / ``ac_cas_get``) and as Script Builder
42+
commands under **Flow**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Comprehensive guides for all AutoControl features.
128128
doc/new_features/v103_features_doc
129129
doc/new_features/v104_features_doc
130130
doc/new_features/v105_features_doc
131+
doc/new_features/v106_features_doc
131132
doc/ocr_backends/ocr_backends_doc
132133
doc/observability/observability_doc
133134
doc/operations_layer/operations_layer_doc
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
樂觀並行版本儲存
2+
==============
3+
4+
``http_conditional`` 以 ETag 做*讀取*快取(``If-None-Match`` / 304),但從不用於*寫入*並行
5+
(``If-Match`` / 版本檢查)。沒有本地的 compare-and-swap / 版本化記錄儲存來做「只在版本未變時更新」。
6+
本功能補上 ETag 故事的寫入面。
7+
8+
純標準函式庫(``json``);不匯入 ``PySide6``。版本為單調整數,儲存為記憶體內並具 JSON 持久化,因此行為
9+
在 CI 中完全具決定性。
10+
11+
無頭 API
12+
--------
13+
14+
.. code-block:: python
15+
16+
from je_auto_control import VersionedStore, VersionConflict, if_match_header
17+
18+
store = VersionedStore()
19+
version = store.put("db.host", "prod-1") # 版本 1
20+
record = store.get("db.host") # {"value": ..., "version": 1}
21+
try:
22+
store.put("db.host", "prod-2", expected_version=record["version"])
23+
except VersionConflict:
24+
reload_and_retry()
25+
header = if_match_header(version) # HTTP If-Match 用 '"1"'
26+
27+
``put`` 僅在 ``expected_version`` 與當前版本相符時寫入(``0`` 要求鍵不存在,省略則為盲寫)並回傳新版本,
28+
過時寫入時拋出 ``VersionConflict``。``get`` 回傳 ``{value, version}``;``delete`` 同樣受保護;``save`` /
29+
``load`` 以 JSON 持久化。``if_match_header`` / ``check_if_match`` 與 ``http_conditional`` 搭配,橋接到真正的
30+
HTTP ``If-Match`` 寫入。
31+
32+
執行器命令
33+
----------
34+
35+
``AC_cas_put`` 回傳 ``{ok, version}``(衝突時 ``{ok: false, error}``);``AC_cas_get`` 回傳 ``{record}``。
36+
兩者使用具名實例登錄,並以 MCP 工具(``ac_cas_put`` / ``ac_cas_get``)以及 Script Builder 中 **Flow** 分類下
37+
的命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ AutoControl 所有功能的完整使用指南。
128128
doc/new_features/v103_features_doc
129129
doc/new_features/v104_features_doc
130130
doc/new_features/v105_features_doc
131+
doc/new_features/v106_features_doc
131132
doc/ocr_backends/ocr_backends_doc
132133
doc/observability/observability_doc
133134
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
@@ -207,6 +207,10 @@
207207
from je_auto_control.utils.dedup_window import DedupWindow
208208
# Per-stream sequence-gap / ordering detection
209209
from je_auto_control.utils.sequence_gap import SequenceTracker
210+
# Optimistic-concurrency versioned store (compare-and-swap / If-Match)
211+
from je_auto_control.utils.optimistic import (
212+
VersionConflict, VersionedStore, check_if_match, if_match_header,
213+
)
210214
# CI workflow annotations (GitHub Actions)
211215
from je_auto_control.utils.ci_annotations import (
212216
emit_annotations, format_annotation,
@@ -935,6 +939,7 @@ def start_autocontrol_gui(*args, **kwargs):
935939
"CircuitBreaker", "CircuitOpenError", "RetryPolicy", "retry_call",
936940
"IdempotencyConflict", "IdempotencyStore", "request_fingerprint",
937941
"DedupWindow", "SequenceTracker",
942+
"VersionConflict", "VersionedStore", "check_if_match", "if_match_header",
938943
"emit_annotations", "format_annotation",
939944
"ClipboardHistory", "default_clipboard_history",
940945
"analyze_heal_log", "heal_stats", "scan_secrets",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,6 +2032,24 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
20322032
),
20332033
description="Classify a sequence number (ok/duplicate/gap/reorder).",
20342034
))
2035+
specs.append(CommandSpec(
2036+
"AC_cas_put", "Flow", "Optimistic: Put (CAS)",
2037+
fields=(
2038+
FieldSpec("name", FieldType.STRING, placeholder="config"),
2039+
FieldSpec("key", FieldType.STRING, placeholder="db.host"),
2040+
FieldSpec("value", FieldType.STRING, placeholder='"prod-1"'),
2041+
FieldSpec("expected_version", FieldType.INT, optional=True),
2042+
),
2043+
description="Put only if expected_version matches (returns new version).",
2044+
))
2045+
specs.append(CommandSpec(
2046+
"AC_cas_get", "Flow", "Optimistic: Get",
2047+
fields=(
2048+
FieldSpec("name", FieldType.STRING, placeholder="config"),
2049+
FieldSpec("key", FieldType.STRING, placeholder="db.host"),
2050+
),
2051+
description="Read a versioned record {value, version}.",
2052+
))
20352053
specs.append(CommandSpec(
20362054
"AC_diff_rows", "Data", "Dataset Diff: Rows by Key",
20372055
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,6 +2932,33 @@ def _rate_limit(name: str, rate: float = 1.0, capacity: float = 1.0,
29322932
_IDEMPOTENCY_STORES: Dict[str, Any] = {}
29332933
_DEDUP_WINDOWS: Dict[str, Any] = {}
29342934
_SEQUENCE_TRACKERS: Dict[str, Any] = {}
2935+
_VERSIONED_STORES: Dict[str, Any] = {}
2936+
2937+
2938+
def _cas_put(name: str, key: str, value: Any,
2939+
expected_version: Any = None) -> Dict[str, Any]:
2940+
"""Adapter: optimistic put into a named versioned store."""
2941+
import json
2942+
from je_auto_control.utils.optimistic import VersionConflict, VersionedStore
2943+
if isinstance(value, str):
2944+
try:
2945+
value = json.loads(value)
2946+
except ValueError:
2947+
pass
2948+
store = _VERSIONED_STORES.setdefault(name, VersionedStore())
2949+
expected = int(expected_version) if expected_version is not None else None
2950+
try:
2951+
version = store.put(key, value, expected_version=expected)
2952+
except VersionConflict as error:
2953+
return {"ok": False, "error": str(error)}
2954+
return {"ok": True, "version": version}
2955+
2956+
2957+
def _cas_get(name: str, key: str) -> Dict[str, Any]:
2958+
"""Adapter: read a record from a named versioned store."""
2959+
from je_auto_control.utils.optimistic import VersionedStore
2960+
store = _VERSIONED_STORES.setdefault(name, VersionedStore())
2961+
return {"record": store.get(key)}
29352962

29362963

29372964
def _sequence_observe(name: str, stream_id: str, seq: Any) -> Dict[str, Any]:
@@ -4586,6 +4613,8 @@ def __init__(self):
45864613
"AC_idempotency_complete": _idempotency_complete,
45874614
"AC_dedup_check": _dedup_check,
45884615
"AC_sequence_observe": _sequence_observe,
4616+
"AC_cas_put": _cas_put,
4617+
"AC_cas_get": _cas_get,
45894618
"AC_detect_drift": _detect_drift,
45904619
"AC_categorical_drift": _categorical_drift,
45914620
"AC_diff_rows": _diff_rows,

0 commit comments

Comments
 (0)