Repository navigation
Expand file tree
/
Copy pathlistener_engine.cpp
More file actions
291 lines (270 loc) · 12.1 KB
/
Copy pathlistener_engine.cpp
File metadata and controls
291 lines (270 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
/*
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
*/
// TASK-108: the accept loop and connection bookkeeping of one native
// listener. See listener_engine.hpp for the design contract.
#include "config.h"
#include <httpserver/detail/resource_scope.hpp>
#include <httpserver/detail/listener_engine.hpp>
#ifdef NATIVE_V3_TLS
#include <httpserver/detail/tls_credentials.hpp>
#include <httpserver/detail/tls_credentials_access.hpp>
#include <httpserver/detail/tls_socket_backend.hpp>
#endif
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <httpserver/detail/io_poll_sys.hpp>
#include <httpserver/detail/diagnostics_access.hpp>
#include <httpserver/detail/drain_scope.hpp>
namespace httpserver {
namespace detail {
namespace {
#ifdef NATIVE_V3_TLS
struct tls_admission_lifetime {
std::shared_ptr<drain_scope> scope_owner;
drain_scope::unit counted;
std::shared_ptr<server::reservation> seat;
server::reservation timer, storage;
tls_admission_lifetime(std::shared_ptr<drain_scope> scope,
std::shared_ptr<server::reservation> connection,
server::reservation timer_seat, server::reservation storage_seat)
: scope_owner(std::move(scope)), counted(*scope_owner), seat(std::move(connection)),
timer(std::move(timer_seat)), storage(std::move(storage_seat)) {}
};
#endif
} // namespace
listener_engine::listener_engine(io_socket_backend& backend, worker_pool& pool,
const server::route_registry& routes,
const server::hook_bus& hooks,
const server::resource_budget& budget,
const server::peer_policy& peers,
drain_scope& scope,
connection_engine_config config)
: backend_(backend),
pool_(pool),
routes_(routes),
hooks_(hooks),
peers_(peers),
owner_(pool),
budget_(child_budget(budget)),
scope_(scope),
config_(std::move(config)) {
}
http::outcome listener_engine::listen(
const server::listener_options& endpoint, std::size_t index,
std::shared_ptr<tls_psk_runtime> runtime, std::shared_ptr<const tls_credentials_binding> binding) {
{
std::lock_guard<std::mutex> lock(mu_);
if (bound_ || stopped_) {
return http::outcome(
http::outcome_code::invalid_state,
"listener_engine: listen() ran twice");
}
}
std::uint16_t port = 0;
const pollsys::native_socket_t transport = pollsys::open_listener(
endpoint.address, endpoint.port, port);
if (transport == pollsys::k_invalid_socket) {
return http::outcome(
http::outcome_code::connection_closed,
"listener_engine: cannot bind "
+ endpoint.address + ":" + std::to_string(endpoint.port));
}
const std::uint64_t id = k_listener_id_base + index;
{
std::lock_guard<std::mutex> lock(mu_);
id_ = id;
pending_listener_ = transport;
endpoint_ = endpoint;
handshake_runtime_ = std::move(runtime);
tls_binding_ = std::move(binding);
bound_port_ = port;
bound_ = true;
}
return http::outcome::okay();
}
void listener_engine::activate() {
{
std::lock_guard lock(mu_);
if (stopped_ || activated_ || !bound_) return;
backend_.adopt_listener(id_, std::exchange(pending_listener_, pollsys::k_invalid_socket));
activated_ = true;
}
spawn(pool_, accept_loop(shared_from_this()), [](task_result<void>) {});
}
void listener_engine::collect_live_locked(
std::vector<std::shared_ptr<connection_engine>>& out) {
out.reserve(out.size() + connections_.size());
for (const auto& entry : connections_) {
out.push_back(entry.second);
}
}
// The drain path (TASK-110): stop accepting and hand every live
// engine its graceful close. The listener release follows the same
// first-caller rule as request_stop(); the engine-side quiesce guard
// sorts out whatever a racing request_stop() already did.
void listener_engine::quiesce(std::chrono::steady_clock::time_point server_deadline) noexcept {
bool release = false;
std::vector<std::shared_ptr<connection_engine>> live;
{
std::lock_guard<std::mutex> lock(mu_);
release = !stopped_;
stopped_ = true;
tls_binding_.reset();
if (pending_listener_ != pollsys::k_invalid_socket) {
pollsys::close_socket(pending_listener_);
bound_port_.store(0, std::memory_order_release);
}
collect_live_locked(live);
}
// Listener first (a pending accept completes connection_closed and
// the loop exits; a racing accept that slips through releases its
// own transport in adopt_accepted), then every live engine.
if (release) backend_.release_connection(id_);
for (const std::shared_ptr<connection_engine>& engine : live) {
engine->quiesce(server_deadline);
}
}
void listener_engine::request_stop(http::outcome_code reason) noexcept {
bool release = false;
std::vector<std::shared_ptr<connection_engine>> live;
{
std::lock_guard<std::mutex> lock(mu_);
release = !stopped_;
stopped_ = true;
tls_binding_.reset();
if (pending_listener_ != pollsys::k_invalid_socket) {
pollsys::close_socket(pending_listener_);
bound_port_.store(0, std::memory_order_release);
}
collect_live_locked(live);
}
// Same release-once rule as quiesce(); the shutdown pass itself
// always runs (each engine's own guard makes it idempotent), so a
// drain's deadline expiry hard-stops even already-quiesced engines.
if (release) backend_.release_connection(id_);
for (const std::shared_ptr<connection_engine>& engine : live) {
engine->shutdown(reason);
}
}
task<void> listener_engine::accept_loop(
std::shared_ptr<listener_engine> self) {
for (;;) {
accept_operation op(self->owner_, self->id_);
op.submit(self->backend_);
const io_result r = co_await std::move(op);
if (r.code != http::outcome_code::ok) {
if (r.code != http::outcome_code::cancelled && r.code != http::outcome_code::connection_closed) {
server::diagnostic_event event{server::diagnostic_kind::connection_rejected, server::diagnostic_reason::transport, r.code};
event.listener_id = self->id_;
diagnostics_access::record(self->config_.diagnostics, event);
}
co_return;
}
self->adopt_accepted(r.accepted_id, r.peer);
}
}
void listener_engine::adopt_accepted(std::uint64_t connection,
const net::peer_address& peer) {
// The policy gate runs before any engine exists (TASK-119 plan
// D1): classify() is one atomic load when the policy is unarmed.
// The verdict is fixed here, THEN the observation fires -- a
// throwing or responding hook cannot change the outcome.
const server::peer_verdict verdict = peers_.classify(peer);
server::accept_decision_ctx seen{peer, verdict.accepted,
verdict.reason};
(void)hooks_.fire<server::hook_phase::accept_decision>(seen);
if (!verdict.accepted) {
server::diagnostic_event event{server::diagnostic_kind::connection_rejected, server::diagnostic_reason::peer_policy, http::outcome_code::peer_refused};
event.listener_id = id_;
event.connection_id = connection;
diagnostics_access::record(config_.diagnostics, event);
// Zero application bytes: the transport is released unread
// (the budget-refusal shape; the refusal wire behavior is a
// pinned migration exception).
backend_.release_connection(connection);
return;
}
std::shared_ptr<connection_engine> engine;
{
std::lock_guard<std::mutex> lock(mu_);
if (stopped_) {
// Stop raced the accept: nobody owns this transport anymore.
backend_.release_connection(connection);
return;
}
auto connection_budget = child_budget(budget_);
connection_admission admission;
admission.seat = std::make_shared<server::reservation>();
if (!connection_budget.reserve(server::resource::connections, 1, *admission.seat).ok()) {
server::diagnostic_event event{server::diagnostic_kind::connection_rejected,
server::diagnostic_reason::capacity, http::outcome_code::limit_exceeded};
event.listener_id = id_;
event.connection_id = connection;
diagnostics_access::record(config_.diagnostics, event);
backend_.release_connection(connection);
return;
}
std::shared_ptr<io_socket_backend> transport;
try {
#ifdef NATIVE_V3_TLS
if (endpoint_.tls) {
server::reservation timer, storage;
// Three fixed record buffers cover input, provider-step output
// and in-flight ciphertext. The lease also retains admission
// and the drain count until child/provider retirement.
if (!connection_budget.reserve(server::resource::timers, 1, timer).ok()
|| !connection_budget.reserve(server::resource::body_buffer_bytes, 3 * 16384, storage).ok()) {
backend_.release_connection(connection);
return;
}
admission.retirement = std::make_shared<tls_admission_lifetime>(
config_.drain_owner, admission.seat, std::move(timer), std::move(storage));
const auto snapshot = tls_credentials_access::acquire(*endpoint_.credentials);
tls_handshake_context handshake{tls_transport::tcp, bound_port(), config_.diagnostics};
handshake.runtime = handshake_runtime_;
handshake.lifetime = admission.retirement;
handshake.allow_http11 = config_.protocols.contains(http::protocol::http_1_1);
handshake.allow_http2 = config_.protocols.contains(http::protocol::http_2);
transport = std::make_shared<tls_socket_backend>(backend_, pool_, connection,
snapshot->select_default(), std::move(handshake));
}
#endif
// The reserved seat bounds TLS allocation and counts the handshake
// in the same engine scope as established H1 work.
engine = std::make_shared<connection_engine>(
backend_, pool_, routes_, hooks_, connection_budget, scope_, config_,
connection, [self = shared_from_this(), connection] { self->erase(connection); }, peer,
&peers_, std::move(transport), std::move(admission));
connections_.emplace(connection, engine);
} catch (...) {
backend_.release_connection(connection);
return;
}
}
// Outside the lock: a budget-refusing engine reports its stop from
// inside start(), and the erase callback takes the same mutex.
engine->start();
}
void listener_engine::erase(std::uint64_t connection) noexcept {
std::lock_guard<std::mutex> lock(mu_);
static_cast<void>(connections_.erase(connection));
}
} // namespace detail
} // namespace httpserver