Repository navigation
Expand file tree
/
Copy pathresponse_writer.hpp
More file actions
295 lines (247 loc) · 12.1 KB
/
Copy pathresponse_writer.hpp
File metadata and controls
295 lines (247 loc) · 12.1 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
/*
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_RESPONSE_WRITER_HPP_
#define SRC_HTTPSERVER_RESPONSE_WRITER_HPP_
// Bounded streaming response-body delivery for the v3 exchange
// (architecture §3.1, DR-V3-003, PRD-V3N-REQ-025/026/027/037). A
// handler that commits a streaming response head with
// exchange::start_response() writes the body through
// exchange::writer(), a response_writer:
//
// - write(chunk) streams one chunk into the engine's bounded output
// queue; exactly one write (or finish) may be outstanding at a
// time; when write resolves ok the whole chunk was accepted;
// - finish(trailers) ends the body; the trailers ride the final
// framing and the writer closes;
// - a write parked on a full queue wakes on the first of queue room,
// engine failure, or exchange disconnect — writes await bounded
// queue capacity, never peer acknowledgement (PRD-V3N-REQ-027).
//
// The engine side of the seam is detail::body_sink, declared in this
// public header for the same reason detail::exchange_sink is: the
// writer's inline methods invoke it and a public header may not
// include a private one. It is engine plumbing, not consumer surface.
// The push model inverts the request side (body_reader): the handler
// produces, the engine drains. A slow transport fills the bounded
// queue; the parked write IS the backpressure. Room appears only while
// the engine drains its queue towards the transport.
//
// Threading contract: writes run on the handler's executor thread (no
// locks); a parked write's completion may arrive from any thread and
// is posted to the writer's executor, mirroring the resume-signal
// rules. Lifetime: write()'s span is consumed before the task resolves
// (the engine copies); moving an exchange with an outstanding write is
// a contract violation (writes happen between moves).
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <coroutine>
#include <memory>
#include <span>
#include <string>
#include <utility>
#include <httpserver/concurrency/cancellation.hpp>
#include <httpserver/http/fields.hpp>
#include <httpserver/http/outcome.hpp>
namespace httpserver {
// Typed result of one response body write. ok: the whole chunk was
// accepted into the engine's bounded output queue (accepted == chunk
// size); cancelled: the exchange disconnected mid-write (accepted is
// the bytes queued before the wake, diagnostic only); every failure
// carries its typed outcome and accepts nothing further.
struct body_write {
http::outcome status;
std::size_t accepted = 0;
};
// Typed result of finish(): ok ends the body (trailers ride the final
// framing); failures are the same typed set as body_write.
struct body_finish {
http::outcome status;
};
namespace detail {
class body_write_wait;
// Verdict of one body_sink::push() / push_end().
enum class body_push : std::uint8_t {
accepted, // bytes copied into the engine's bounded output queue
full, // queue at capacity; the writer must park
failed, // transport/protocol failure; see body_sink::failure()
};
// What a parked body_write_wait completes with.
enum class body_room : std::uint8_t {
freed, // output queue room freed; re-push
failed,
cancelled, // the exchange's stop fired before completion
};
// A push verdict plus the byte count copied into the output queue.
// The count rides beside the enum because the writer must track the
// accepted prefix of a chunk, which a bare verdict cannot express.
struct body_push_result {
body_push kind = body_push::full;
std::size_t copied = 0; // bytes accepted this push (push_end: 0)
};
// Engine-facing seam of one streaming response 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 output queue drained towards its transport; the
// writer pushes bounded chunks into it.
class body_sink {
public:
virtual ~body_sink() = default;
// Copies at most from.size() bytes into the output queue and
// returns the verdict plus the count copied. Room appears only
// while the engine drains its queue (never as peer
// acknowledgement). Zero-byte accepted pushes are forbidden; the
// writer re-pushes them.
virtual body_push_result push(std::span<const std::byte> from) = 0;
// Ends the body; `trailers` ride the final framing. The end marker
// occupies one queue slot, so a full queue reports full. Exactly
// once per sink; the writer guarantees the single call.
virtual body_push_result push_end(const http::fields& trailers) = 0;
// Failure diagnostic; meaningful once a push returned failed.
virtual const http::outcome& failure() const noexcept = 0;
// Parks `wait` until the FIRST of: queue room freed, 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 sink.
virtual void park(body_write_wait& wait) = 0;
// Detaches a still-parked waiter (the waiter was destroyed without
// waking). After unpark, the sink must forget the waiter.
virtual void unpark(body_write_wait& wait) = 0;
};
// Shared state of one parked response write. 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.
//
// Deliberately shaped after body_reader.hpp's wait_node/body_wait: the
// near-duplication (flagged by the CPD lint next to that file's own
// task.hpp baseline) unifies only when a third consumer justifies a
// shared detail seam — the two nodes differ in result vocabulary
// (body_wake vs body_room) and seam direction (pull vs push), and a
// shared header would couple the request and response delivery areas
// before the engines that own them exist.
struct write_wait_node {
std::atomic<bool> delivered{false};
body_room result = body_room::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 response write (frame-resident awaitable, shaped after
// detail::body_wait). The writer co_awaits it; the seam completes it
// at most once. The destructor detaches a still-parked waiter, so
// destroying the writer's frame (task abandoned) can never leave a
// dangling waiter registered with the engine.
class body_write_wait final {
public:
body_write_wait(body_sink* sink, stop_token cancel);
body_write_wait(const body_write_wait&) = delete;
body_write_wait& operator=(const body_write_wait&) = delete;
body_write_wait(body_write_wait&&) = delete;
body_write_wait& operator=(body_write_wait&&) = delete;
body_write_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_room await_resume() const noexcept { return node_->result; }
// Completion entry for body_sink 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_room room) 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_write_wait();
private:
friend class http2_body_stream;
body_sink* sink_;
std::shared_ptr<write_wait_node> node_;
task_frame_base* frame_ = nullptr;
stop_token cancel_;
};
} // namespace detail
// The streaming response body of an exchange. Obtained only from
// exchange::writer(); operations are legal only after a successful
// start_response() and fail typed otherwise. One exchange, one writer;
// movable with the exchange, never copied.
//
// Writer state (no locks — writes run on the handler's executor
// thread): started (engine seam attached by start_response), pending
// (the one-outstanding-write rule), finished (push_end accepted; the
// body is over), failed (sticky: an engine failure ends the body),
// closed (writer shut, by finish or exchange abort). A write issued
// after a disconnect fails connection_closed carrying the stored
// disconnect detail; a write interrupted by one returns cancelled.
class response_writer {
public:
// Streams one chunk into the engine's bounded output queue,
// suspending while the queue is full and resuming when room frees.
// Resolves ok only once the whole chunk was accepted; a partial
// acceptance surfaces as further internal pushes. An empty chunk
// fails invalid_argument without touching the engine.
task<body_write> write(std::span<const std::byte> data);
// Ends the response body. `trailers` ride the final framing (empty
// when none). On ok the writer is closed; further write/finish fail
// invalid_state. Exactly-once: the sink sees push_end at most once
// (the one-outstanding gate serializes entry; a cancelled finish
// never half-ends the body).
task<body_finish> finish(http::fields trailers = http::fields());
response_writer(response_writer&&) noexcept = default;
response_writer& operator=(response_writer&&) noexcept = default;
response_writer(const response_writer&) = delete;
response_writer& operator=(const response_writer&) = delete;
private:
friend class exchange;
explicit response_writer(stop_token cancel) noexcept
: cancel_token_(std::move(cancel)) { }
// exchange::start_response, after the engine accepted the head. A
// null sink keeps every write at invalid_state.
void activate(detail::body_sink* sink) noexcept;
// exchange::abort, and finish itself on success: the writer shuts
// down and every later operation fails typed. Idempotent bool, so
// the body ends exactly once no matter the finish/abort ordering.
void close() noexcept { closed_ = true; }
// exchange::disconnect, before the stop fan-out: stores the detail
// that writes 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 write operations: fails typed and
// touches nothing when a push may not start.
http::outcome enter(bool empty_chunk) const;
// Sticky failure: the body is unusable after this point; later
// writes return the same outcome without touching the engine.
void fail_sticky(const http::outcome& reason) noexcept;
detail::body_sink* sink_ = nullptr;
stop_token cancel_token_;
http::outcome failure_;
http::outcome disconnect_reason_;
bool started_ = false;
bool pending_ = false;
bool finished_ = false;
bool failed_ = false;
bool closed_ = false;
};
} // namespace httpserver
#endif // SRC_HTTPSERVER_RESPONSE_WRITER_HPP_