|
1 | | -// docs/architecture.md |
| 1 | +# Architecture |
| 2 | + |
| 3 | +## Component Overview |
| 4 | + |
| 5 | +``` |
| 6 | + ┌──────────────────────────────────┐ |
| 7 | + │ rate-limiter pod │ |
| 8 | + Client ─ POST──> │ HTTP: 8081 gRPC: 50051 │ |
| 9 | + │ │ │ │ |
| 10 | + │ └──────┬─────┘ │ |
| 11 | + │ │ │ |
| 12 | + │ limiter.AllowRequest() │ |
| 13 | + │ │ │ |
| 14 | + │ store.AllowRequest() │ |
| 15 | + │ │ │ |
| 16 | + └──────────────┼───────────────────┘ |
| 17 | + │ EVALSHA (Lua) |
| 18 | + ▼ |
| 19 | + ┌──────────────────────────────────┐ |
| 20 | + │ Redis │ |
| 21 | + │ sorted set per API key │ |
| 22 | + │ sliding_window.lua (atomic) │ |
| 23 | + └──────────────────────────────────┘ |
| 24 | +``` |
| 25 | + |
| 26 | +## Request Lifecycle |
| 27 | + |
| 28 | +1. Client sends `POST \check` with `X-API-KEY: <key>` header. |
| 29 | +2. HTTP handler calls `limiter.AllowRequest(ctx, apiKey)`. |
| 30 | +3. Limiter looks up the `Policy` for the key (per-key override of default: 100 req/min). |
| 31 | +4. Limiter calls `store.AllowRequest(ctx, key, policy)`. |
| 32 | +5. Store issues `EVALSHA <sha> rl:<key> <now_ms> <window_ms> <limit>` to Redis. |
| 33 | +6. Lua script atomically decides allow (1) or reject (0). |
| 34 | +7. Store returns `(bool, error)` to limiter. |
| 35 | +8. Metrics recorded: `ratelimiter_requests_total{status=allowed|rejected} |
| 36 | +9. HTTP handler writes `200 OK` or `429 Too Many Requests` with JSON body. |
| 37 | + |
| 38 | +## Kubernetes deployment |
| 39 | + |
| 40 | +``` |
| 41 | +┌───────────────────────────────────────────────────┐ |
| 42 | +│ Kubernetes cluster │ |
| 43 | +│ │ |
| 44 | +│ ┌─────────────────────────────┐ │ |
| 45 | +│ │ rate-limiter Deployment │ │ |
| 46 | +│ │ replicas: 3 (HPA: 3–10) │ │ |
| 47 | +│ │ │ │ |
| 48 | +│ │ pod-0 pod-1 pod-2 │ │ |
| 49 | +│ └──────────┬──────────────────┘ │ |
| 50 | +│ │ ClusterIP Service :8080/:50051 │ |
| 51 | +│ ▼ │ |
| 52 | +│ ┌──────────────────────────┐ │ |
| 53 | +│ │ Redis StatefulSet │ │ |
| 54 | +│ │ redis-0 (PVC: 1Gi) │ │ |
| 55 | +│ └──────────────────────────┘ │ |
| 56 | +│ │ |
| 57 | +│ Prometheus ─── scrapes /metrics on each pod │ |
| 58 | +│ Grafana ─── dashboard: deploy/grafana/ │ |
| 59 | +└───────────────────────────────────────────────────┘ |
| 60 | +``` |
| 61 | + |
| 62 | +## Package dependency graph |
| 63 | + |
| 64 | +``` |
| 65 | +cmd/server |
| 66 | + ├── internal/config (load env/yaml, policy map) |
| 67 | + ├── internal/limiter (AllowRequest — core domain) |
| 68 | + │ └── internal/store (Store interface) |
| 69 | + │ └── Redis + scripts/lua/ |
| 70 | + ├── internal/metrics (Prometheus counters + histograms) |
| 71 | + ├── internal/grpc (thin gRPC handler) |
| 72 | + └── internal/http (thin HTTP handler) |
| 73 | +``` |
| 74 | + |
| 75 | +No circular dependencies. `internal/limiter` defines the `Store` interface it depends on (dependency inversion) - this is why `store/store.go` imports `internal/limiter` rather than the other way around. |
| 76 | + |
| 77 | +## Test Strategy |
| 78 | + |
| 79 | +| Layer | Tool | Infrastructure | |
| 80 | +|-------|------|----------------| |
| 81 | +| Algorithm unit tests | `go test` | None (pure Go) | |
| 82 | +| Limiter unit tests | `go test` with `FakeStore` | None | |
| 83 | +| Store integration tests | `go test -tags=integration` | Live Redis | |
| 84 | +| Concurrency correctness | `go test -tags=integration` | Live Redis + running service | |
| 85 | +| Load / latency | k6, vegeta | Live Redis + running service | |
| 86 | +| Failure scenarios | Manual (documented in failure_test.go) | k8s cluster | |
0 commit comments