Repository navigation
Expand file tree
/
Copy pathbody_reader.hpp
More file actions
310 lines (258 loc) · 12.4 KB
/
Copy pathbody_reader.hpp
File metadata and controls
310 lines (258 loc) · 12.4 KB
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/*
This file is part of libhttpserver
Copyright (C) 2011-2026 Sebastiano Merlino
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the file
LICENSE in the distribution; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA
*/
#ifndef SRC_HTTPSERVER_BODY_READER_HPP_
#define SRC_HTTPSERVER_BODY_READER_HPP_
// Bounded request-body delivery for the v3 exchange (architecture
// §3.1, DR-V3-003, PRD-V3N-REQ-021/022/025). A handler that admitted a
// body reads it through exchange::body(), a body_reader:
//
// - read_some(destination) performs one bounded incremental read;
// exactly one read (or collect) may be outstanding at a time;
// - collect(maximum) buffers the whole body and fails limit_exceeded
// exactly when the body exceeds the cap (exactly-at-cap succeeds);
// - a read parked with nothing staged wakes on the first of bytes
// staged, end of body, engine failure, or exchange disconnect.
//
// A bodyless request is admitted like any other: the engine accepts the
// admission but has no body to deliver, so it provides no source. An
// admitted body with no source is therefore complete and empty -- the
// first read ends the body and a collect returns zero bytes -- never an
// error (the sync route adapter depends on this to serve bodyless
// requests with an empty span).
//
// The engine side of the seam is detail::body_source, declared in this
// public header for the same reason detail::exchange_sink is: the
// reader's inline methods invoke it and a public header may not
// include a private one. It is engine plumbing, not consumer surface.
// The pull model makes consumption and receive credit the same event:
// credit for staged bytes is released only inside pull(), so a handler
// that stops reading applies backpressure by never draining the
// engine's bounded staging queue.
//
// Threading contract: reads run on the handler's executor thread (no
// locks); a parked read's completion may arrive from any thread and is
// posted to the reader's executor, mirroring the resume-signal rules.
// Lifetime: body_read::data views the caller's buffer; moving an
// exchange with an outstanding read is a contract violation (reads
// happen between moves).
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <coroutine>
#include <memory>
#include <span>
#include <string>
#include <utility>
#include <vector>
#include <httpserver/concurrency/cancellation.hpp>
#include <httpserver/http/fields.hpp>
#include <httpserver/http/outcome.hpp>
namespace httpserver {
// Typed result of one bounded incremental body read. On a successful
// data read, `data` views the caller's destination buffer (never
// empty) and stays valid until the buffer is reused or destroyed.
struct body_read {
http::outcome status;
std::span<const std::byte> data;
bool end_of_body = false;
};
// Typed result of collect(maximum). ok carries the whole remaining
// body, size() <= maximum; limit_exceeded carries nothing.
struct body_collect {
http::outcome status;
std::vector<std::byte> data;
};
namespace detail {
class body_wait;
// Verdict of one body_source::pull().
enum class body_pull : std::uint8_t {
data, // staged bytes copied into the destination
end, // authoritative end of body; trailers are final
empty, // nothing staged; the reader must park
failed, // framing/protocol failure; see body_source::failure()
};
// What a parked body_wait completes with.
enum class body_wake : std::uint8_t {
data, end, failed,
cancelled, // the exchange's stop fired before completion
};
// A pull verdict plus the byte count copied into the destination. The
// count rides beside the enum because the reader must hand the caller
// exactly the consumed prefix, which a bare verdict cannot express.
struct body_pull_result {
body_pull kind = body_pull::empty;
std::size_t copied = 0;
};
// Engine-facing seam of one admitted body. Implemented once per
// protocol engine (the HTTP/1 engine arrives with the request-handling
// tasks); fakes implement it in tests. The engine owns the bounded
// staging queue fed by its transport reads; the reader pulls bounded
// chunks out of it.
class body_source {
public:
virtual ~body_source() = default;
// Moves at most into.size() staged bytes into `into` and returns
// the verdict plus the count copied. Returning data is THE
// consumption event: implementations release receive credit
// (backpressure relief) for exactly the bytes copied here — never
// for bytes merely staged. Zero-byte pulls returning data are
// forbidden; the reader skips them.
virtual body_pull_result pull(std::span<std::byte> into) = 0;
// Received trailers; final once a pull returned end.
virtual const http::fields& trailers() const noexcept = 0;
// Failure diagnostic; meaningful once a pull returned failed.
virtual const http::outcome& failure() const noexcept = 0;
// Parks `wait` until the FIRST of: bytes staged (data), end
// reached, failure raised, or the exchange's stop requested
// (cancelled). Completes at most once, posted to the waiter's
// executor (never inline on the completing thread). Precondition:
// at most one waiter parked per source.
virtual void park(body_wait& wait) = 0;
// Detaches a still-parked waiter (the waiter was destroyed without
// waking). After unpark, the source must forget the waiter.
virtual void unpark(body_wait& wait) = 0;
};
// Shared state of one parked body read. claim() resolves the wait
// exactly once: a second completion, or a completion racing the
// waiter's destruction, loses the CAS and is a no-op.
struct wait_node {
std::atomic<bool> delivered{false};
body_wake result = body_wake::cancelled;
std::coroutine_handle<> awaiting;
std::shared_ptr<frame_witness> witness; // null for foreign coroutines
executor* target = nullptr; // null resumes witness-guarded
bool claim() noexcept;
// Posts the resumption to the waiter's executor. Only when no
// executor is known does the resumption run inline, still guarded
// by the frame witness.
void resume_posted() noexcept;
};
// One parked body read (frame-resident awaitable, shaped after the
// resume waiter). The reader co_awaits it; the seam completes it at
// most once. The destructor detaches a still-parked waiter, so
// destroying the reader's frame (task abandoned) can never leave a
// dangling waiter registered with the engine.
class body_wait final {
public:
body_wait(body_source* source, stop_token cancel);
body_wait(const body_wait&) = delete;
body_wait& operator=(const body_wait&) = delete;
body_wait(body_wait&&) = delete;
body_wait& operator=(body_wait&&) = delete;
body_wait& bind_frame(task_frame_base* frame) noexcept;
bool await_ready() const noexcept { return false; }
std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting);
body_wake await_resume() const noexcept { return node_->result; }
// Completion entry for body_source implementations (any thread).
// Exactly-once via the claim; a losing later completion is a
// no-op. The posted job owns the node, so it stays safe across
// waiter destruction.
void complete(body_wake wake) noexcept;
// The exchange's disconnect token: the seam completes a parked
// wait as cancelled when it fires.
const stop_token& cancel_token() const noexcept { return cancel_; }
~body_wait();
private:
friend class http2_body_stream;
body_source* source_;
std::shared_ptr<wait_node> node_;
task_frame_base* frame_ = nullptr;
stop_token cancel_;
};
} // namespace detail
// The admitted request body of an exchange. Obtained only from
// exchange::body(); operations are legal only after a successful
// admit_body() and fail typed otherwise. One exchange, one reader;
// movable with the exchange, never copied.
//
// Reader state (no locks — reads run on the handler's executor
// thread): admitted (engine seam attached; a bodyless request is
// admitted with no seam and reads as an empty complete body), pending
// (the one-outstanding-read rule), at_end (trailers final), failed
// (sticky: over-limit or engine failure ends the body), closed
// (exchange terminal). A read issued after a disconnect fails
// connection_closed carrying the stored disconnect detail; a read
// interrupted by one returns cancelled.
class body_reader {
public:
// Bounded incremental read: resolves with at least one byte (and
// at most destination.size()) unless the body ends first. An empty
// destination fails invalid_argument without touching the engine.
task<body_read> read_some(std::span<std::byte> destination);
// The received trailers; final once a read returned end_of_body
// (or a collect succeeded). Empty when none were received.
const http::fields& trailers() const noexcept;
// Fully buffers the remaining body, at most `maximum` bytes. Fails
// limit_exceeded (diagnostic naming the cap) exactly when the body
// exceeds it; exactly-at-cap succeeds. On success the reader is at
// end of body and trailers() is final. A bodyless request (admitted
// with no engine source) collects to zero bytes successfully.
task<body_collect> collect(std::uint64_t maximum);
body_reader(body_reader&& other) noexcept = default;
body_reader& operator=(body_reader&& other) noexcept = default;
body_reader(const body_reader&) = delete;
body_reader& operator=(const body_reader&) = delete;
private:
friend class exchange;
explicit body_reader(stop_token cancel) noexcept
: cancel_token_(std::move(cancel)) { }
// exchange::admit_body, after the engine accepted the admission. A
// null source is the bodyless-request shape: the engine accepted the
// admission but has no body to deliver (see bodyless()).
void activate(detail::body_source* source) noexcept;
// exchange::respond/upgrade, before committing: the exchange is
// terminal and the body is over.
void close() noexcept { closed_ = true; }
// exchange::disconnect, before the stop fan-out: stores the detail
// that reads issued after the disconnect report with their typed
// connection_closed failure.
void note_disconnect(const http::outcome& reason) noexcept {
disconnect_reason_ = reason;
}
// Shared entry gate of the read operations: fails typed and
// touches nothing when a pull may not start.
http::outcome enter(bool empty_destination) const;
// Sticky failure: the body is unusable after this point; later
// reads return the same outcome without touching the engine.
void fail_sticky(const http::outcome& reason) noexcept;
// An admission the engine accepted without a delivery source: the
// request carries no body (the engine's bodyless construction), so
// the body is empty and already complete.
bool bodyless() const noexcept { return admitted_ && source_ == nullptr; }
// Pull size for collect: never more than the remaining cap room,
// plus one probe byte that proves an over-limit body exactly. The
// probe may consume one byte of the tripping pull, which is
// discarded; the outcome stays exact.
static std::size_t collect_want(std::uint64_t room) noexcept {
return room < COLLECT_CHUNK_BYTES
? static_cast<std::size_t>(room) + 1
: COLLECT_CHUNK_BYTES;
}
static constexpr std::size_t COLLECT_CHUNK_BYTES = 512;
detail::body_source* source_ = nullptr;
stop_token cancel_token_;
http::outcome failure_;
http::outcome disconnect_reason_;
bool admitted_ = false;
bool pending_ = false;
bool at_end_ = false;
bool failed_ = false;
bool closed_ = false;
};
} // namespace httpserver
#endif // SRC_HTTPSERVER_BODY_READER_HPP_