1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! Rate limiting at the level a real service needs, over a store Kynos does not
//! ship.
//!
//! ```text
//! cargo run -p kynos --example rate_limit --no-default-features \
//! --features openapi31,macros,server,http1,json
//! ```
//!
//! The split is the same one `examples/jwt.rs` makes about tokens. Kynos owns
//! the algorithm, the 429, the `Retry-After`, the headers and the description.
//! What it does not own is *where the counters live* — that is a dependency,
//! and one service wants a process-local cache while the next wants Redis
//! shared across a fleet. `moka` here is a dev-dependency of this file, named
//! nowhere under `src/`.
//!
//! Six things are worth noticing:
//!
//! * **The store is two methods.** `read` and `increment`, neither taking a
//! closure across an `await`. That is what keeps a Redis backend
//! implementable: a richer seam — compare-and-swap, or a transaction — has no
//! portable equivalent, and would have quietly made this a moka-only trait.
//! * **The window slides.** A fixed window lets a client spend a full quota at
//! the end of one and a full quota at the start of the next, which is twice
//! the advertised rate. The estimate weights the previous window by how much
//! of it is still in view, which needs one extra read and no request log.
//! * **A refusal spends nothing.** The counter is read before it is
//! incremented, so a throttled client that keeps retrying does not push its
//! own window along forever.
//! * **Several quotas compose.** A per-second burst and a per-day allowance are
//! both enforced and both reported. The `X-RateLimit-*` triple has room for
//! one, which is why `standard_fields` exists.
//! * **`Retry-After` is solved, not guessed.** The delay is when the estimate
//! actually falls below the ceiling, assuming the client sends nothing more.
//! Reporting a window's *length* instead would be a number the service does
//! not require.
//! * **A store outage is not an API outage.** The default on failure is to
//! allow. A limiter exists to shed load, and one that sheds everything when
//! its cache blinks has turned a degradation into an incident.
//! * **The refusal names its own problem type.** `about:blank` says the status
//! code is the whole story, which is false of a service enforcing three
//! different quotas. The URI is stated once, as a type, and reaches both the
//! 429's body and the 429 the description declares.
use ;
use ;
/// A counter store over `moka`.
///
/// The whole implementation, and it is short on purpose: a store that needed
/// more than this from the seam would be a store the seam had failed.
///
/// `moka`'s per-entry expiry is what makes `ttl` meaningful. A backend without
/// one — a plain `HashMap` — would grow without bound, which is why the trait
/// asks for a TTL rather than leaving eviction to the caller.
/// A store over an infallible cache cannot fail, and says so.
/// The problem type every refusal from this service carries.
///
/// A marker rather than a value: what an interceptor declares is read from its
/// associated types, so a URI chosen per denial would reach the wire and leave
/// the description saying `about:blank` about it.
;
// --- The operations -------------------------------------------------------
/// An ordinary read, under the shared limit.
async
/// An expensive write, under a tighter limit of its own.
async
async