From 4084a966fb598d01157e830863fb806f82b69161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Gonz=C3=A1lez=20Di=20Antonio?= Date: Sat, 11 Jul 2026 15:20:16 +0200 Subject: [PATCH] docs: add Mermaid architecture, flow, and lifecycle diagrams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add rendered-on-GitHub Mermaid diagrams to the README and token-bucket guide to make the library's design easier to grasp at a glance: - README: a component flowchart of BucketLimiter / Storage / factory / sweeper and a GetOrAdd().Allow() sequence diagram showing per-key isolation. - docs/TOKEN_BUCKET.md: a token-bucket flow diagram, a per-key lifecycle state diagram (Absent → Active → Idle → evicted), and an HTTP-middleware sequence diagram matching the ReserveN/DelayFrom/Cancel flow in examples/middleware. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 67 ++++++++++++++++++++++++++++++++++++++++++++ docs/TOKEN_BUCKET.md | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/README.md b/README.md index 3dd2c8f..48f8757 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,73 @@ idle for a configurable duration. - **HTTP middleware example** — with accurate `Retry-After` and `RateLimit-*` response headers. +## Architecture + +`BucketLimiter` is a thin manager: it maps each key to its own `Limiter`, +builds new ones on demand through a factory, persists them in a pluggable +`Storage`, and runs a single background goroutine that evicts idle keys. + +```mermaid +flowchart TD + subgraph caller["Your code"] + C["GetOrAdd(key)"] + end + + subgraph manager["BucketLimiter[K]"] + direction TB + F["newLimiter func() Limiter
(factory)"] + A["access map
K → last-use time"] + S["sweepLoop goroutine
evicts idle keys"] + end + + subgraph store["Storage[K, Limiter]"] + direction LR + K1["user-123 → bucket"] + K2["user-456 → bucket"] + K3["10.0.0.7 → bucket"] + end + + C -->|"1. Load / LoadOrStore"| store + C -.->|"2. build on miss"| F + F -.->|"fresh *rate.Limiter"| store + C -->|"3. touch"| A + S -->|"Delete idle"| store + S -->|"Delete idle"| A + + K1 & K2 & K3 -->|"independent
token buckets"| RL["golang.org/x/time/rate"] +``` + +Each key owns an **independent** token bucket, so one client draining its +budget has no effect on any other. A typical `GetOrAdd(key).Allow()` call: + +```mermaid +sequenceDiagram + autonumber + participant App as Your code + participant BL as BucketLimiter + participant St as Storage + participant Lim as Limiter (bucket) + + App->>BL: GetOrAdd(key) + BL->>St: Load(key) + alt key exists + St-->>BL: existing Limiter + else first use of key + BL->>BL: newLimiter() + BL->>St: LoadOrStore(key, fresh) + Note over BL,St: atomic — racing callers
share one instance + St-->>BL: stored Limiter + end + BL->>BL: touch(key) — refresh idle timer + BL-->>App: Limiter + App->>Lim: Allow() + alt token available + Lim-->>App: true (consume 1 token) + else bucket empty + Lim-->>App: false (rate limited → 429) + end +``` + ## Installation ```bash diff --git a/docs/TOKEN_BUCKET.md b/docs/TOKEN_BUCKET.md index 86f1f83..02bc4e8 100644 --- a/docs/TOKEN_BUCKET.md +++ b/docs/TOKEN_BUCKET.md @@ -53,6 +53,19 @@ Picture a bucket that holds tokens: request limited (bucket empty) ``` +The same idea as a flow diagram: + +```mermaid +flowchart TD + R["Refill: +r tokens / second"] -->|"drip"| B + B{"Bucket
capacity = burst b"} + B -->|"overflow above b"| X["discarded"] + Req(["Incoming request
needs 1 token"]) --> Q{"tokens ≥ 1 ?"} + Q -->|"yes"| Take["consume 1 token"] --> Allowed(["✅ allowed"]) + Q -->|"no"| Limited(["⛔ limited — reject or wait"]) + B -.->|"current level"| Q +``` + Rules: 1. Tokens are added to the bucket at a steady **rate** `r` (tokens per second). @@ -208,6 +221,24 @@ full bucket. Choose `deleteAfter` comfortably longer than the window over which you want the limit to hold (e.g. minutes, not milliseconds) so a client cannot reset its own bucket by pausing briefly. +The lifecycle of a single key: + +```mermaid +stateDiagram-v2 + [*] --> Absent + Absent --> Active: GetOrAdd(key)
newLimiter() builds a fresh bucket + Active --> Active: GetOrAdd(key)
refreshes idle timer + Active --> Idle: no access for deleteAfter + Idle --> Active: GetOrAdd(key)
before the sweep runs + Idle --> Absent: sweepLoop evicts
(state discarded) + Active --> Absent: Remove(key) + Absent --> [*] +``` + +The sweep is not instantaneous, so a key idle past `deleteAfter` lingers until +the next tick — worst case ~1.5·`deleteAfter` after its last use with the +default interval. + ## HTTP response headers Well-behaved HTTP clients can self-throttle if you tell them the state of their @@ -227,6 +258,34 @@ the example also emits the legacy `X-RateLimit-*` variants for older clients. [RFC 9110 §10.2.3](https://www.rfc-editor.org/rfc/rfc9110#section-10.2.3) and is computed from the reservation delay so it is accurate rather than a guess. +End to end, a request through the middleware: + +```mermaid +sequenceDiagram + autonumber + participant Client + participant MW as Middleware + participant BL as BucketLimiter + participant Lim as Limiter (per-IP bucket) + participant H as Handler + + Client->>MW: HTTP request + MW->>MW: extract client IP
(net.SplitHostPort) + MW->>BL: GetOrAdd(ip) + BL-->>MW: Limiter + MW->>Lim: ReserveN(now, 1) + alt token available now (delay == 0) + Lim-->>MW: reservation, DelayFrom(now) = 0 + MW->>H: serve + H-->>MW: response + MW-->>Client: 200 OK
RateLimit-Limit / Remaining / Reset + else bucket empty (delay > 0) + Lim-->>MW: reservation, DelayFrom(now) = d + MW->>Lim: reservation.Cancel()
(return the token) + MW-->>Client: 429 Too Many Requests
Retry-After: ⌈d⌉ + end +``` + ## Comparison with other algorithms | Algorithm | Bursts | Memory/key | Notes |