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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//! The response-bytes ceiling: `[validation] max_response_bytes`.
//!
//! # Why a byte ceiling rather than a bigger complexity score
//!
//! GATE-1 scores a *document* — depth, complexity, aliases — and multiplies a
//! nested selection by its page size at every level
//! (`graphql::complexity::DocumentAnalyzer::field_complexity`). That arithmetic
//! models an engine that resolves each nesting level with its own unit of work.
//!
//! This engine does not have one. A nested field on a materialised view is
//! already inside the parent document, and
//! [`project_nested_lists`](crate::runtime::project_nested_lists) projects it out
//! of rows that have *already been read* — one read, whatever the depth. Scoring
//! such a read by multiply-per-level over-estimates it by orders of magnitude and
//! would be bounding an execution model this framework does not use.
//!
//! What a materialised read actually costs is rows × document bytes. That is the
//! quantity this module bounds, and it is the only one that means the same thing
//! on every transport: a GraphQL document, a REST `?select=`, a gRPC column read
//! and an NDJSON export all end in rows.
//!
//! Depth and complexity keep their job — they guard fields that *are* resolved at
//! runtime, and malformed documents. They are not the control that bounds work on
//! a read that is one row fetch.
//!
//! # Why it is charged after the read and not scored before it
//!
//! Size is not recoverable from the request. The same query against the same view
//! returns a kilobyte for one tenant and a gigabyte for another, and no pre-read
//! estimate can tell them apart without reading. `max_page_size` (#421) bounds the
//! row count and `max_operation_cost` (#379) bounds what the request asks for;
//! neither knows what a row weighs.
//!
//! So the ceiling is **resolved** at the read chokepoint — it travels on the
//! resolved read like every other decision that belongs to the read rather than to
//! its delivery — and **charged** wherever the rows arrive. That split is the same
//! one `6a709bc1f` used for the per-row field gate: `RowReadPlan` carries the gate,
//! and the streaming arm applies it per frame so the transport holds no policy
//! decision of its own.
//!
//! A buffered read is charged once, on the rows the adapter returned. A streamed
//! read is charged per frame and cut at the frame that crosses the ceiling — which
//! is the point of doing it on the stream at all: it is the only arm where the
//! server would otherwise keep producing after the answer is already too big.
use ;
use ;
use crate;
/// Structural overhead charged for one JSON object or array: the two brackets.
const BRACKETS: u64 = 2;
/// Structural overhead charged per object member: two quotes around the key, the
/// colon, and the comma that separates it from the next one.
const MEMBER_PUNCTUATION: u64 = 4;
/// Structural overhead charged per array element: the separating comma.
const ELEMENT_PUNCTUATION: u64 = 1;
/// Charged for a JSON string on top of its contents: the two quote characters.
///
/// Escaping is not modelled. A string of control characters or quotes serialises
/// longer than this counts, so the estimate can run under the true wire size on
/// pathological content. See the accuracy note on [`json_bytes`].
const QUOTES: u64 = 2;
/// Bytes charged for a JSON number.
///
/// `serde_json` keeps a number as `u64`/`i64`/`f64`, so the exact serialised width
/// is only knowable by formatting it. Formatting every number of every row to
/// measure the row is a second full serialisation of the response, which is the
/// cost this ceiling exists to avoid paying twice; 20 is the width of the widest
/// `u64` and therefore an upper bound for the integer cases.
const NUMBER_BYTES: u64 = 20;
/// Bytes charged for `null`, and for `true`/`false` (4 and 5 — the wider is used).
const LITERAL_BYTES: u64 = 5;
/// Estimated serialised size of a JSON value, in bytes.
///
/// # Accuracy
///
/// This is an estimate, deliberately. An exact count means serialising the value
/// to measure it and then serialising it again to send it, on every row — the
/// response would be built twice to enforce a ceiling on its size.
///
/// The estimate is within a few percent of the wire size for the documents this
/// engine actually returns, and the ways it can be wrong are bounded and known:
/// numbers are charged at the widest `u64` width (over-counts small integers), and
/// string escaping is not modelled (under-counts a string made of quotes or
/// control characters). Neither moves the total by an order of magnitude, which is
/// the resolution a `DoS` ceiling needs.
///
/// Callers that need the exact figure — an HTTP `Content-Length`, say — must
/// measure the serialised bytes; this is not that number and is not documented as
/// one.
///
/// # Recursion
///
/// Depth is bounded before this runs: `serde_json` refuses to parse beyond its own
/// nesting limit, so a value reaching here has already been accepted by the
/// parser. There is no separate guard.
/// Estimated serialised size of one row-shaped column value, in bytes.
///
/// The row transports (gRPC) encode a column as protobuf rather than JSON, so the
/// punctuation constants above do not apply. What is charged is the payload — the
/// only part that scales with the data — plus a fixed width for the scalars.
pub const
/// A running total of delivered bytes, against the ceiling the read resolved.
///
/// Charged as rows arrive. **Whoever holds one decides what the ceiling bounds**, and
/// the rule is the same at every scale: the budget is kept for as long as the thing
/// being bounded lasts.
///
/// * a buffered read owns one for the length of that read, so the ceiling bounds the read;
/// * a streamed read keeps one across its whole lifetime, so the ceiling bounds the response rather
/// than each frame independently;
/// * a request whose transport issues **several** reads on its behalf — a REST `?select=` embed
/// resolves one sub-read per parent row per level — passes one *borrowed* budget to all of them,
/// so the ceiling bounds the request rather than each sub-read independently.
///
/// That last case is why `used` is atomic and [`charge`](Self::charge) takes `&self`:
/// sharing a budget is holding it by reference, not a second type with a second copy of
/// the rule. A budget rebuilt per read bounds each read and leaves their aggregate
/// unbounded, which is a ceiling that reads as enforced and is not.
///
/// Deliberately **not** `Clone`. A clone is an unshared copy with its own zeroed total,
/// which is precisely the mistake the paragraph above describes, and it would compile
/// silently.