Skip to content

Commit 2543374

Browse files
authored
Merge pull request #9 from slashdevops/docs/mermaid-diagrams
docs: add Mermaid architecture, flow, and lifecycle diagrams
2 parents d1a88bd + 4084a96 commit 2543374

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,73 @@ idle for a configurable duration.
4141
- **HTTP middleware example** — with accurate `Retry-After` and `RateLimit-*`
4242
response headers.
4343

44+
## Architecture
45+
46+
`BucketLimiter` is a thin manager: it maps each key to its own `Limiter`,
47+
builds new ones on demand through a factory, persists them in a pluggable
48+
`Storage`, and runs a single background goroutine that evicts idle keys.
49+
50+
```mermaid
51+
flowchart TD
52+
subgraph caller["Your code"]
53+
C["GetOrAdd(key)"]
54+
end
55+
56+
subgraph manager["BucketLimiter[K]"]
57+
direction TB
58+
F["newLimiter func() Limiter<br/>(factory)"]
59+
A["access map<br/>K → last-use time"]
60+
S["sweepLoop goroutine<br/>evicts idle keys"]
61+
end
62+
63+
subgraph store["Storage[K, Limiter]"]
64+
direction LR
65+
K1["user-123 → bucket"]
66+
K2["user-456 → bucket"]
67+
K3["10.0.0.7 → bucket"]
68+
end
69+
70+
C -->|"1. Load / LoadOrStore"| store
71+
C -.->|"2. build on miss"| F
72+
F -.->|"fresh *rate.Limiter"| store
73+
C -->|"3. touch"| A
74+
S -->|"Delete idle"| store
75+
S -->|"Delete idle"| A
76+
77+
K1 & K2 & K3 -->|"independent<br/>token buckets"| RL["golang.org/x/time/rate"]
78+
```
79+
80+
Each key owns an **independent** token bucket, so one client draining its
81+
budget has no effect on any other. A typical `GetOrAdd(key).Allow()` call:
82+
83+
```mermaid
84+
sequenceDiagram
85+
autonumber
86+
participant App as Your code
87+
participant BL as BucketLimiter
88+
participant St as Storage
89+
participant Lim as Limiter (bucket)
90+
91+
App->>BL: GetOrAdd(key)
92+
BL->>St: Load(key)
93+
alt key exists
94+
St-->>BL: existing Limiter
95+
else first use of key
96+
BL->>BL: newLimiter()
97+
BL->>St: LoadOrStore(key, fresh)
98+
Note over BL,St: atomic — racing callers<br/>share one instance
99+
St-->>BL: stored Limiter
100+
end
101+
BL->>BL: touch(key) — refresh idle timer
102+
BL-->>App: Limiter
103+
App->>Lim: Allow()
104+
alt token available
105+
Lim-->>App: true (consume 1 token)
106+
else bucket empty
107+
Lim-->>App: false (rate limited → 429)
108+
end
109+
```
110+
44111
## Installation
45112

46113
```bash

docs/TOKEN_BUCKET.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,19 @@ Picture a bucket that holds tokens:
5353
request limited (bucket empty)
5454
```
5555

56+
The same idea as a flow diagram:
57+
58+
```mermaid
59+
flowchart TD
60+
R["Refill: +r tokens / second"] -->|"drip"| B
61+
B{"Bucket<br/>capacity = burst b"}
62+
B -->|"overflow above b"| X["discarded"]
63+
Req(["Incoming request<br/>needs 1 token"]) --> Q{"tokens ≥ 1 ?"}
64+
Q -->|"yes"| Take["consume 1 token"] --> Allowed(["✅ allowed"])
65+
Q -->|"no"| Limited(["⛔ limited — reject or wait"])
66+
B -.->|"current level"| Q
67+
```
68+
5669
Rules:
5770

5871
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
208221
you want the limit to hold (e.g. minutes, not milliseconds) so a client cannot
209222
reset its own bucket by pausing briefly.
210223

224+
The lifecycle of a single key:
225+
226+
```mermaid
227+
stateDiagram-v2
228+
[*] --> Absent
229+
Absent --> Active: GetOrAdd(key)<br/>newLimiter() builds a fresh bucket
230+
Active --> Active: GetOrAdd(key)<br/>refreshes idle timer
231+
Active --> Idle: no access for deleteAfter
232+
Idle --> Active: GetOrAdd(key)<br/>before the sweep runs
233+
Idle --> Absent: sweepLoop evicts<br/>(state discarded)
234+
Active --> Absent: Remove(key)
235+
Absent --> [*]
236+
```
237+
238+
The sweep is not instantaneous, so a key idle past `deleteAfter` lingers until
239+
the next tick — worst case ~1.5·`deleteAfter` after its last use with the
240+
default interval.
241+
211242
## HTTP response headers
212243

213244
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.
227258
[RFC 9110 §10.2.3](https://www.rfc-editor.org/rfc/rfc9110#section-10.2.3) and is
228259
computed from the reservation delay so it is accurate rather than a guess.
229260

261+
End to end, a request through the middleware:
262+
263+
```mermaid
264+
sequenceDiagram
265+
autonumber
266+
participant Client
267+
participant MW as Middleware
268+
participant BL as BucketLimiter
269+
participant Lim as Limiter (per-IP bucket)
270+
participant H as Handler
271+
272+
Client->>MW: HTTP request
273+
MW->>MW: extract client IP<br/>(net.SplitHostPort)
274+
MW->>BL: GetOrAdd(ip)
275+
BL-->>MW: Limiter
276+
MW->>Lim: ReserveN(now, 1)
277+
alt token available now (delay == 0)
278+
Lim-->>MW: reservation, DelayFrom(now) = 0
279+
MW->>H: serve
280+
H-->>MW: response
281+
MW-->>Client: 200 OK<br/>RateLimit-Limit / Remaining / Reset
282+
else bucket empty (delay > 0)
283+
Lim-->>MW: reservation, DelayFrom(now) = d
284+
MW->>Lim: reservation.Cancel()<br/>(return the token)
285+
MW-->>Client: 429 Too Many Requests<br/>Retry-After: ⌈d⌉
286+
end
287+
```
288+
230289
## Comparison with other algorithms
231290

232291
| Algorithm | Bursts | Memory/key | Notes |

0 commit comments

Comments
 (0)