Skip to content

Commit 8458e86

Browse files
authored
Merge pull request #270 from Integration-Automation/feat/rate-limit
Add client-side rate limiting
2 parents 7d27357 + f7f3c0a commit 8458e86

15 files changed

Lines changed: 480 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-21) — Client-Side Rate Limiting](#whats-new-2026-06-21--client-side-rate-limiting)
1617
- [What's new (2026-06-21) — JSON Web Tokens (JWT)](#whats-new-2026-06-21--json-web-tokens-jwt)
1718
- [What's new (2026-06-21) — License Policy Gate](#whats-new-2026-06-21--license-policy-gate)
1819
- [What's new (2026-06-21) — OpenVEX Vulnerability Triage](#whats-new-2026-06-21--openvex-vulnerability-triage)
@@ -114,6 +115,12 @@
114115

115116
---
116117

118+
## What's new (2026-06-21) — Client-Side Rate Limiting
119+
120+
Stay under API quotas. Full reference: [`docs/source/Eng/doc/new_features/v62_features_doc.rst`](docs/source/Eng/doc/new_features/v62_features_doc.rst).
121+
122+
- **`TokenBucket` / `SlidingWindowLimiter` / `throttle`** (`AC_rate_limit`, `ac_rate_limit`): `RetryPolicy`/`CircuitBreaker` recover from failures but nothing shaped the *rate* of calls. This adds a token bucket (smooth rate + burst), a sliding-window limiter (Cloudflare's O(1) weighted counter), and a leading-edge throttle decorator. Every limiter takes an injectable `clock` (and `acquire` a `sleep`) so it's fully deterministic in CI with no real delays. `AC_rate_limit` gates an action against a named bucket, returning `{acquired, tokens, wait}`.
123+
117124
## What's new (2026-06-21) — JSON Web Tokens (JWT)
118125

119126
Mint and verify bearer tokens for the APIs you automate. Full reference: [`docs/source/Eng/doc/new_features/v61_features_doc.rst`](docs/source/Eng/doc/new_features/v61_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-21) — 客户端速率限制](#本次更新-2026-06-21--客户端速率限制)
1516
- [本次更新 (2026-06-21) — JSON Web Token(JWT)](#本次更新-2026-06-21--json-web-tokenjwt)
1617
- [本次更新 (2026-06-21) — 许可证策略闸门](#本次更新-2026-06-21--许可证策略闸门)
1718
- [本次更新 (2026-06-21) — OpenVEX 漏洞分级](#本次更新-2026-06-21--openvex-漏洞分级)
@@ -113,6 +114,12 @@
113114

114115
---
115116

117+
## 本次更新 (2026-06-21) — 客户端速率限制
118+
119+
守在 API 配额之内。完整参考:[`docs/source/Zh/doc/new_features/v62_features_doc.rst`](../docs/source/Zh/doc/new_features/v62_features_doc.rst)
120+
121+
- **`TokenBucket` / `SlidingWindowLimiter` / `throttle`**(`AC_rate_limit``ac_rate_limit`):`RetryPolicy`/`CircuitBreaker` 从失败中复原,但没有任何东西塑形调用的*速率*。本功能补上 token bucket(平滑速率 + 突发)、sliding-window 限制器(Cloudflare 的 O(1) 加权计数)以及前缘 throttle 装饰器。每个限制器都接受可注入的 `clock`(`acquire` 另接受 `sleep`),因此在 CI 完全确定、没有真正延迟。`AC_rate_limit` 以具名 bucket 闸控动作,返回 `{acquired, tokens, wait}`
122+
116123
## 本次更新 (2026-06-21) — JSON Web Token(JWT)
117124

118125
为你自动化的 API 签发与验证 bearer token。完整参考:[`docs/source/Zh/doc/new_features/v61_features_doc.rst`](../docs/source/Zh/doc/new_features/v61_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-21) — 用戶端速率限制](#本次更新-2026-06-21--用戶端速率限制)
1516
- [本次更新 (2026-06-21) — JSON Web Token(JWT)](#本次更新-2026-06-21--json-web-tokenjwt)
1617
- [本次更新 (2026-06-21) — 授權政策閘門](#本次更新-2026-06-21--授權政策閘門)
1718
- [本次更新 (2026-06-21) — OpenVEX 漏洞分級](#本次更新-2026-06-21--openvex-漏洞分級)
@@ -113,6 +114,12 @@
113114

114115
---
115116

117+
## 本次更新 (2026-06-21) — 用戶端速率限制
118+
119+
守在 API 配額之內。完整參考:[`docs/source/Zh/doc/new_features/v62_features_doc.rst`](../docs/source/Zh/doc/new_features/v62_features_doc.rst)
120+
121+
- **`TokenBucket` / `SlidingWindowLimiter` / `throttle`**(`AC_rate_limit``ac_rate_limit`):`RetryPolicy`/`CircuitBreaker` 從失敗中復原,但沒有任何東西塑形呼叫的*速率*。本功能補上 token bucket(平滑速率 + 突發)、sliding-window 限制器(Cloudflare 的 O(1) 加權計數)以及前緣 throttle 裝飾器。每個限制器都接受可注入的 `clock`(`acquire` 另接受 `sleep`),因此在 CI 完全具決定性、沒有真正延遲。`AC_rate_limit` 以具名 bucket 閘控動作,回傳 `{acquired, tokens, wait}`
122+
116123
## 本次更新 (2026-06-21) — JSON Web Token(JWT)
117124

118125
為你自動化的 API 簽發與驗證 bearer token。完整參考:[`docs/source/Zh/doc/new_features/v61_features_doc.rst`](../docs/source/Zh/doc/new_features/v61_features_doc.rst)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
Client-Side Rate Limiting
2+
=========================
3+
4+
The framework had ``RetryPolicy`` / ``CircuitBreaker`` (which *recover* from
5+
failures) and a FIFO ``work_queue``, but nothing to shape the *rate* of calls —
6+
so a flow hammering an external API had no way to stay under a quota. This adds
7+
the two standard limiters plus a leading-edge throttle, all with an injectable
8+
clock so they are deterministic in tests (no real sleeping).
9+
10+
* :class:`TokenBucket` — a smooth rate with burst capacity (lazy refill).
11+
* :class:`SlidingWindowLimiter` — a fixed call budget per rolling window
12+
(Cloudflare's O(1) weighted-counter approximation).
13+
* :func:`throttle` — a decorator that fires a function at most once per interval.
14+
15+
Pure standard library (``threading`` for the lock, ``time`` only as the default
16+
clock); imports no ``PySide6``.
17+
18+
Headless API
19+
------------
20+
21+
.. code-block:: python
22+
23+
from je_auto_control import TokenBucket, SlidingWindowLimiter, throttle
24+
25+
# 5 requests/second, bursts up to 10
26+
bucket = TokenBucket(rate=5, capacity=10)
27+
if bucket.try_acquire():
28+
call_api() # non-blocking: skip / queue if False
29+
bucket.acquire() # or block until a token frees up
30+
31+
# at most 100 calls per 60s rolling window
32+
window = SlidingWindowLimiter(limit=100, window_s=60)
33+
if window.try_acquire():
34+
call_api()
35+
36+
@throttle(2.0) # fire at most once every 2 seconds
37+
def on_event(payload):
38+
...
39+
40+
``TokenBucket.try_acquire`` takes tokens if available; ``acquire`` blocks (with
41+
an optional ``timeout``); ``time_until_available`` reports the wait so a
42+
scheduler can pace itself. Every limiter accepts a ``clock=`` (and ``acquire``
43+
a ``sleep=``) so the whole thing is exercised in CI with a fake clock — no real
44+
delays.
45+
46+
Executor command
47+
----------------
48+
49+
``AC_rate_limit`` takes a limiter ``name`` plus ``rate`` / ``capacity`` / ``n``
50+
and tries to take ``n`` tokens from that named token bucket (created on first
51+
use), returning ``{acquired, tokens, wait}`` so a flow can gate or defer an
52+
action. The same operation is exposed as the MCP tool ``ac_rate_limit`` and as a
53+
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
@@ -84,6 +84,7 @@ Comprehensive guides for all AutoControl features.
8484
doc/new_features/v59_features_doc
8585
doc/new_features/v60_features_doc
8686
doc/new_features/v61_features_doc
87+
doc/new_features/v62_features_doc
8788
doc/ocr_backends/ocr_backends_doc
8889
doc/observability/observability_doc
8990
doc/operations_layer/operations_layer_doc
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
用戶端速率限制
2+
=============
3+
4+
框架過去有 ``RetryPolicy`` / ``CircuitBreaker``(用於從失敗中*復原*)以及 FIFO 的
5+
``work_queue``,卻沒有任何東西能塑形呼叫的*速率* —— 因此猛打外部 API 的流程沒有辦法守在配額
6+
之內。本功能補上兩個標準限制器,外加一個前緣觸發的 throttle,全都以可注入的時鐘實作,因此在測試
7+
中具決定性(不需真的睡眠)。
8+
9+
* :class:`TokenBucket` —— 平滑速率搭配突發容量(惰性回填)。
10+
* :class:`SlidingWindowLimiter` —— 每個滾動視窗固定的呼叫額度(Cloudflare 的 O(1) 加權計數
11+
近似)。
12+
* :func:`throttle` —— 一個讓函式在每個間隔內最多觸發一次的裝飾器。
13+
14+
純標準函式庫(``threading`` 用於鎖,``time`` 僅作為預設時鐘);不匯入 ``PySide6``。
15+
16+
無頭 API
17+
--------
18+
19+
.. code-block:: python
20+
21+
from je_auto_control import TokenBucket, SlidingWindowLimiter, throttle
22+
23+
# 每秒 5 個請求,可突發到 10
24+
bucket = TokenBucket(rate=5, capacity=10)
25+
if bucket.try_acquire():
26+
call_api() # 非阻塞:False 時略過 / 排入佇列
27+
bucket.acquire() # 或阻塞直到有 token 釋出
28+
29+
# 每 60 秒滾動視窗最多 100 次呼叫
30+
window = SlidingWindowLimiter(limit=100, window_s=60)
31+
if window.try_acquire():
32+
call_api()
33+
34+
@throttle(2.0) # 每 2 秒最多觸發一次
35+
def on_event(payload):
36+
...
37+
38+
``TokenBucket.try_acquire`` 在有 token 時取用;``acquire`` 會阻塞(可選 ``timeout``);
39+
``time_until_available`` 回報等待時間,讓排程器自行調速。每個限制器都接受 ``clock=``(``acquire``
40+
另接受 ``sleep=``),因此整體可在 CI 以假時鐘演練 —— 沒有真正的延遲。
41+
42+
執行器命令
43+
----------
44+
45+
``AC_rate_limit`` 接受限制器 ``name`` 以及 ``rate`` / ``capacity`` / ``n``,嘗試從該具名 token
46+
bucket(首次使用時建立)取用 ``n`` 個 token,回傳 ``{acquired, tokens, wait}``,讓流程可閘控或
47+
延後某個動作。同一操作亦以 MCP 工具 ``ac_rate_limit`` 以及 Script Builder 中 **Flow** 分類下的
48+
命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ AutoControl 所有功能的完整使用指南。
8484
doc/new_features/v59_features_doc
8585
doc/new_features/v60_features_doc
8686
doc/new_features/v61_features_doc
87+
doc/new_features/v62_features_doc
8788
doc/ocr_backends/ocr_backends_doc
8889
doc/observability/observability_doc
8990
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
@@ -320,6 +320,10 @@
320320
ClaimsPolicy, ExpiredTokenError, InvalidSignatureError, JwtError,
321321
decode_jwt, encode_jwt,
322322
)
323+
# Client-side rate limiting (token bucket / sliding window / throttle)
324+
from je_auto_control.utils.rate_limit import (
325+
SlidingWindowLimiter, TokenBucket, throttle,
326+
)
323327
# Background popup/interrupt watchdog (unattended automation)
324328
from je_auto_control.utils.watchdog import (
325329
PopupWatchdog, WatchdogRule, default_popup_watchdog,
@@ -798,6 +802,7 @@ def start_autocontrol_gui(*args, **kwargs):
798802
"license_findings_to_sarif", "normalize_spdx",
799803
"ClaimsPolicy", "ExpiredTokenError", "InvalidSignatureError", "JwtError",
800804
"decode_jwt", "encode_jwt",
805+
"SlidingWindowLimiter", "TokenBucket", "throttle",
801806
# MCP server
802807
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
803808
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,16 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
13631363
),
13641364
description="Run 'actions' (JSON view) via a named circuit breaker.",
13651365
))
1366+
specs.append(CommandSpec(
1367+
"AC_rate_limit", "Flow", "Rate Limit (Token Bucket)",
1368+
fields=(
1369+
FieldSpec("name", FieldType.STRING),
1370+
FieldSpec("rate", FieldType.FLOAT, optional=True, default=1.0),
1371+
FieldSpec("capacity", FieldType.FLOAT, optional=True, default=1.0),
1372+
FieldSpec("n", FieldType.FLOAT, optional=True, default=1.0),
1373+
),
1374+
description="Try to take 'n' tokens from a named limiter; {acquired, wait}.",
1375+
))
13661376

13671377

13681378
def _add_input_macro_specs(specs: List[CommandSpec]) -> None:

je_auto_control/utils/executor/action_executor.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2914,6 +2914,20 @@ def _check_licenses(components: Any, allow: Any = None,
29142914
return {"violations": violations, "count": len(violations)}
29152915

29162916

2917+
_RATE_LIMITERS: Dict[str, Any] = {}
2918+
2919+
2920+
def _rate_limit(name: str, rate: float = 1.0, capacity: float = 1.0,
2921+
n: float = 1.0) -> Dict[str, Any]:
2922+
"""Adapter: try to take ``n`` tokens from a named token-bucket limiter."""
2923+
from je_auto_control.utils.rate_limit import TokenBucket
2924+
bucket = _RATE_LIMITERS.setdefault(
2925+
name, TokenBucket(float(rate), float(capacity)))
2926+
acquired = bucket.try_acquire(float(n))
2927+
return {"acquired": acquired, "tokens": round(bucket.tokens, 4),
2928+
"wait": round(bucket.time_until_available(float(n)), 4)}
2929+
2930+
29172931
def _jwt_encode(claims: Any, key: str, alg: str = "HS256") -> Dict[str, Any]:
29182932
"""Adapter: sign a compact JWT from claims (a dict or JSON string)."""
29192933
import json
@@ -3724,6 +3738,7 @@ def __init__(self):
37243738
"AC_check_licenses": _check_licenses,
37253739
"AC_jwt_encode": _jwt_encode,
37263740
"AC_jwt_decode": _jwt_decode,
3741+
"AC_rate_limit": _rate_limit,
37273742
"AC_generate_sop": _generate_sop,
37283743
"AC_tween_drag": _tween_drag,
37293744
"AC_list_plugins": _list_plugins,

0 commit comments

Comments
 (0)