|
| 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**. |
0 commit comments