kino 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- data/Cargo.lock +115 -24
- data/README.md +113 -41
- data/doc/architecture.md +61 -7
- data/doc/benchmarks.md +120 -26
- data/ext/kino/Cargo.toml +8 -3
- data/ext/kino/src/control.rs +267 -60
- data/ext/kino/src/env_strings.rs +172 -12
- data/ext/kino/src/io_shards.rs +437 -0
- data/ext/kino/src/lib.rs +12 -7
- data/ext/kino/src/listen.rs +4 -1
- data/ext/kino/src/log.rs +10 -2
- data/ext/kino/src/queue.rs +61 -44
- data/ext/kino/src/registry.rs +103 -10
- data/ext/kino/src/request.rs +150 -69
- data/ext/kino/src/response.rs +40 -9
- data/ext/kino/src/server.rs +759 -97
- data/ext/kino/src/tls.rs +129 -12
- data/lib/kino/configuration.rb +16 -0
- data/lib/kino/server.rb +10 -0
- data/lib/kino/templates/kino.rb.tt +13 -2
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +14 -11
- data/sig/kino.rbs +3 -0
- metadata +16 -1
data/ext/kino/src/server.rs
CHANGED
|
@@ -9,12 +9,13 @@ use std::time::Duration;
|
|
|
9
9
|
|
|
10
10
|
use http_body_util::BodyExt;
|
|
11
11
|
use hyper::service::service_fn;
|
|
12
|
-
use hyper_util::rt::TokioIo;
|
|
12
|
+
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
|
|
13
|
+
use hyper_util::server::conn::auto;
|
|
13
14
|
use magnus::{Error, Ruby};
|
|
14
15
|
use parking_lot::{Mutex, RwLock};
|
|
15
16
|
|
|
16
17
|
use crate::listen::Listener;
|
|
17
|
-
use crate::registry::{self, BoxedCtx, ServerInner, WorkerSlot};
|
|
18
|
+
use crate::registry::{self, BoxedCtx, RuntimeHandle, ServerInner, WorkerSlot};
|
|
18
19
|
use crate::request::RequestCtx;
|
|
19
20
|
use crate::response::{plain_response, HyperResponse, Responder};
|
|
20
21
|
|
|
@@ -56,10 +57,14 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
56
57
|
let request_timeout_ms: u64 = cfg_opt::<u64>(ruby, config, "request_timeout_ms")?.unwrap_or(0);
|
|
57
58
|
let max_body_size: usize = cfg_opt::<usize>(ruby, config, "max_body_size")?.unwrap_or(0);
|
|
58
59
|
let max_connections: usize = cfg_opt::<usize>(ruby, config, "max_connections")?.unwrap_or(1024);
|
|
60
|
+
let io_shards: bool = cfg_opt(ruby, config, "io_shards")?.unwrap_or(false);
|
|
61
|
+
let io_threads: usize = cfg_opt::<usize>(ruby, config, "io_threads")?.unwrap_or(0);
|
|
59
62
|
let tokio_threads: usize = cfg_opt::<usize>(ruby, config, "tokio_threads")?.unwrap_or(0);
|
|
60
63
|
let tls_cert: Option<String> = cfg_opt(ruby, config, "tls_cert")?;
|
|
61
64
|
let tls_key: Option<String> = cfg_opt(ruby, config, "tls_key")?;
|
|
62
65
|
let lanes: bool = cfg_opt(ruby, config, "lanes")?.unwrap_or(false);
|
|
66
|
+
// Default true guards embedders calling the native layer directly.
|
|
67
|
+
let http2: bool = cfg_opt(ruby, config, "http2")?.unwrap_or(true);
|
|
63
68
|
let log_requests: bool = cfg_opt(ruby, config, "log_requests")?.unwrap_or(false);
|
|
64
69
|
let mode: String = cfg_opt(ruby, config, "mode")?.unwrap_or_else(|| "threaded".to_string());
|
|
65
70
|
let workers: usize = cfg_opt(ruby, config, "workers")?.unwrap_or(0);
|
|
@@ -67,7 +72,7 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
67
72
|
let batch: usize = cfg_opt(ruby, config, "batch")?.unwrap_or(1);
|
|
68
73
|
let acceptor = match (&tls_cert, &tls_key) {
|
|
69
74
|
(Some(cert), Some(key)) => Some(
|
|
70
|
-
crate::tls::build_acceptor(cert, key)
|
|
75
|
+
crate::tls::build_acceptor(cert, key, http2)
|
|
71
76
|
.map_err(|e| Error::new(ruby.exception_runtime_error(), format!("TLS: {e}")))?,
|
|
72
77
|
),
|
|
73
78
|
(None, None) => None,
|
|
@@ -79,8 +84,7 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
79
84
|
}
|
|
80
85
|
};
|
|
81
86
|
|
|
82
|
-
let listener =
|
|
83
|
-
Listener::bind(&bind, port).map_err(|e| io_error(ruby, "bind failed", e))?;
|
|
87
|
+
let listener = Listener::bind(&bind, port).map_err(|e| io_error(ruby, "bind failed", e))?;
|
|
84
88
|
// Ruby refuses this combination up front; this guards embedders
|
|
85
89
|
// calling the native layer directly.
|
|
86
90
|
if acceptor.is_some() && matches!(listener, Listener::Unix(..)) {
|
|
@@ -106,15 +110,6 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
106
110
|
})
|
|
107
111
|
.transpose()?;
|
|
108
112
|
|
|
109
|
-
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
|
110
|
-
builder.enable_all().thread_name("kino-tokio");
|
|
111
|
-
if tokio_threads > 0 {
|
|
112
|
-
builder.worker_threads(tokio_threads);
|
|
113
|
-
}
|
|
114
|
-
let runtime = builder
|
|
115
|
-
.build()
|
|
116
|
-
.map_err(|e| io_error(ruby, "tokio runtime failed", e))?;
|
|
117
|
-
|
|
118
113
|
let (req_tx, req_rx) = flume::bounded(queue_depth);
|
|
119
114
|
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
|
|
120
115
|
|
|
@@ -123,7 +118,7 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
123
118
|
req_tx: Mutex::new(Some(req_tx)),
|
|
124
119
|
req_rx,
|
|
125
120
|
shutdown_tx,
|
|
126
|
-
runtime: Mutex::new(None),
|
|
121
|
+
runtime: Mutex::new(RuntimeHandle::None),
|
|
127
122
|
slots: RwLock::new(Vec::new()),
|
|
128
123
|
in_flight: std::sync::atomic::AtomicUsize::new(0),
|
|
129
124
|
served: std::sync::atomic::AtomicU64::new(0),
|
|
@@ -135,8 +130,14 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
135
130
|
state: std::sync::atomic::AtomicU8::new(registry::STATE_BOOTING),
|
|
136
131
|
respawns: std::sync::atomic::AtomicU64::new(0),
|
|
137
132
|
quarantine_replacements: std::sync::atomic::AtomicU64::new(0),
|
|
138
|
-
topology: registry::Topology {
|
|
133
|
+
topology: registry::Topology {
|
|
134
|
+
mode,
|
|
135
|
+
workers,
|
|
136
|
+
threads,
|
|
137
|
+
batch,
|
|
138
|
+
},
|
|
139
139
|
https: acceptor.is_some(),
|
|
140
|
+
http2,
|
|
140
141
|
unix_path,
|
|
141
142
|
access_log: log_requests.then(|| crate::logsink::Sink::new(std::io::stdout())),
|
|
142
143
|
lanes,
|
|
@@ -145,18 +146,48 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
145
146
|
queue_histogram: registry::QueueHistogram::new(),
|
|
146
147
|
});
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
149
|
+
if io_shards {
|
|
150
|
+
// The shards keep serving accepted connections while the acceptor
|
|
151
|
+
// drains on `shutdown_rx`; this second signal stops them only at
|
|
152
|
+
// final teardown.
|
|
153
|
+
let (runtime_shutdown_tx, runtime_shutdown_rx) = tokio::sync::watch::channel(false);
|
|
154
|
+
let threads = crate::io_shards::spawn(
|
|
155
|
+
listener,
|
|
156
|
+
acceptor,
|
|
157
|
+
server.clone(),
|
|
158
|
+
max_connections,
|
|
159
|
+
shutdown_rx,
|
|
160
|
+
runtime_shutdown_rx,
|
|
161
|
+
crate::io_shards::thread_count(io_threads),
|
|
162
|
+
)
|
|
163
|
+
.map_err(|e| io_error(ruby, "sharded runtime failed", e))?;
|
|
164
|
+
*server.runtime.lock() = RuntimeHandle::Shards {
|
|
165
|
+
shutdown_tx: runtime_shutdown_tx,
|
|
166
|
+
threads,
|
|
167
|
+
};
|
|
168
|
+
} else {
|
|
169
|
+
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
|
170
|
+
builder.enable_all().thread_name("kino-tokio");
|
|
171
|
+
if tokio_threads > 0 {
|
|
172
|
+
builder.worker_threads(tokio_threads);
|
|
173
|
+
}
|
|
174
|
+
let runtime = builder
|
|
175
|
+
.build()
|
|
176
|
+
.map_err(|e| io_error(ruby, "tokio runtime failed", e))?;
|
|
177
|
+
let tokio_listener = {
|
|
178
|
+
let _guard = runtime.enter();
|
|
179
|
+
AsyncListener::from_std(listener)
|
|
180
|
+
.map_err(|e| io_error(ruby, "listener setup failed", e))?
|
|
181
|
+
};
|
|
182
|
+
runtime.spawn(accept_loop(
|
|
183
|
+
tokio_listener,
|
|
184
|
+
acceptor,
|
|
185
|
+
server.clone(),
|
|
186
|
+
max_connections,
|
|
187
|
+
shutdown_rx,
|
|
188
|
+
));
|
|
189
|
+
*server.runtime.lock() = RuntimeHandle::MultiThread(runtime);
|
|
190
|
+
}
|
|
160
191
|
|
|
161
192
|
let id = server.id;
|
|
162
193
|
let control_port = match control_bind {
|
|
@@ -170,10 +201,11 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
170
201
|
Some(bind) => match crate::control::start(bind, server.clone(), control_token) {
|
|
171
202
|
Ok(port) => port,
|
|
172
203
|
Err(e) => {
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
204
|
+
// Nothing is serving yet, so the bound only matters for a
|
|
205
|
+
// wedged shard thread; the default runtime just cancels
|
|
206
|
+
// its one idle accept task.
|
|
207
|
+
let _ = server.shutdown_tx.send(true);
|
|
208
|
+
std::mem::take(&mut *server.runtime.lock()).shutdown(Duration::from_millis(1_000));
|
|
177
209
|
return Err(io_error(ruby, "control start failed", e));
|
|
178
210
|
}
|
|
179
211
|
},
|
|
@@ -200,22 +232,24 @@ const UNIX_LOCAL: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST),
|
|
|
200
232
|
|
|
201
233
|
/// The accept loop's listener: TCP (optionally behind TLS), or a unix
|
|
202
234
|
/// socket, which carries plain HTTP only.
|
|
203
|
-
enum AsyncListener {
|
|
235
|
+
pub(crate) enum AsyncListener {
|
|
204
236
|
Tcp(tokio::net::TcpListener),
|
|
205
237
|
Unix(tokio::net::UnixListener),
|
|
206
238
|
}
|
|
207
239
|
|
|
208
240
|
/// One accepted connection, before the protocol layer sees it.
|
|
209
|
-
enum Conn {
|
|
241
|
+
pub(crate) enum Conn {
|
|
210
242
|
Tcp(tokio::net::TcpStream),
|
|
211
243
|
Unix(tokio::net::UnixStream),
|
|
212
244
|
}
|
|
213
245
|
|
|
214
246
|
impl AsyncListener {
|
|
215
247
|
/// Register the bound listener with the current runtime.
|
|
216
|
-
fn from_std(listener: Listener) -> std::io::Result<AsyncListener> {
|
|
248
|
+
pub(crate) fn from_std(listener: Listener) -> std::io::Result<AsyncListener> {
|
|
217
249
|
Ok(match listener {
|
|
218
|
-
Listener::Tcp(listener) =>
|
|
250
|
+
Listener::Tcp(listener) => {
|
|
251
|
+
AsyncListener::Tcp(tokio::net::TcpListener::from_std(listener)?)
|
|
252
|
+
}
|
|
219
253
|
Listener::Unix(listener, _) => {
|
|
220
254
|
AsyncListener::Unix(tokio::net::UnixListener::from_std(listener)?)
|
|
221
255
|
}
|
|
@@ -223,7 +257,7 @@ impl AsyncListener {
|
|
|
223
257
|
}
|
|
224
258
|
|
|
225
259
|
/// The next connection with its (peer, local) addresses.
|
|
226
|
-
async fn accept(&self) -> std::io::Result<(Conn, SocketAddr, SocketAddr)> {
|
|
260
|
+
pub(crate) async fn accept(&self) -> std::io::Result<(Conn, SocketAddr, SocketAddr)> {
|
|
227
261
|
match self {
|
|
228
262
|
AsyncListener::Tcp(listener) => {
|
|
229
263
|
let (stream, remote_addr) = listener.accept().await?;
|
|
@@ -274,34 +308,107 @@ async fn accept_loop(
|
|
|
274
308
|
tokio::spawn(async move {
|
|
275
309
|
// Held for the connection's lifetime; dropping it frees a slot.
|
|
276
310
|
let _permit = permit;
|
|
277
|
-
|
|
278
|
-
(Conn::Tcp(stream), Some(acceptor)) => {
|
|
279
|
-
// Handshake failures (port scans, plain HTTP to a TLS
|
|
280
|
-
// port) and stalled handshakes (slowloris) just drop the
|
|
281
|
-
// connection; the timeout bounds the latter.
|
|
282
|
-
let handshake = tokio::time::timeout(TLS_HANDSHAKE_TIMEOUT, acceptor.accept(stream));
|
|
283
|
-
let Ok(Ok(tls)) = handshake.await else { return };
|
|
284
|
-
serve_connection(tls, server, remote_addr, local_addr).await;
|
|
285
|
-
}
|
|
286
|
-
(Conn::Tcp(stream), None) => {
|
|
287
|
-
serve_connection(stream, server, remote_addr, local_addr).await
|
|
288
|
-
}
|
|
289
|
-
// TLS over a unix socket is refused at bind time.
|
|
290
|
-
(Conn::Unix(stream), _) => {
|
|
291
|
-
serve_connection(stream, server, remote_addr, local_addr).await
|
|
292
|
-
}
|
|
293
|
-
}
|
|
311
|
+
serve_conn(conn, acceptor, server, remote_addr, local_addr).await;
|
|
294
312
|
});
|
|
295
313
|
}
|
|
296
314
|
}
|
|
297
315
|
|
|
316
|
+
/// Everything between an accepted connection and hyper: the optional TLS
|
|
317
|
+
/// handshake, then the protocol layer. Shared by the default accept loop
|
|
318
|
+
/// and the sharded one, so connection policy exists exactly once.
|
|
319
|
+
pub(crate) async fn serve_conn(
|
|
320
|
+
conn: Conn,
|
|
321
|
+
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
322
|
+
server: Arc<ServerInner>,
|
|
323
|
+
remote_addr: SocketAddr,
|
|
324
|
+
local_addr: SocketAddr,
|
|
325
|
+
) {
|
|
326
|
+
match (conn, acceptor) {
|
|
327
|
+
(Conn::Tcp(stream), Some(acceptor)) => {
|
|
328
|
+
// Handshake failures (port scans, plain HTTP to a TLS
|
|
329
|
+
// port) and stalled handshakes (slowloris) just drop the
|
|
330
|
+
// connection; the timeout bounds the latter.
|
|
331
|
+
let handshake = tokio::time::timeout(TLS_HANDSHAKE_TIMEOUT, acceptor.accept(stream));
|
|
332
|
+
let Ok(Ok(tls)) = handshake.await else { return };
|
|
333
|
+
serve_connection(tls, server, remote_addr, local_addr).await;
|
|
334
|
+
}
|
|
335
|
+
(Conn::Tcp(stream), None) => {
|
|
336
|
+
serve_connection(stream, server, remote_addr, local_addr).await
|
|
337
|
+
}
|
|
338
|
+
// TLS over a unix socket is refused at bind time.
|
|
339
|
+
(Conn::Unix(stream), _) => serve_connection(stream, server, remote_addr, local_addr).await,
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
298
343
|
/// Slowloris guard: drop a connection that has not sent its complete request
|
|
299
344
|
/// headers within this window. Long enough never to trip a real client (even
|
|
300
345
|
/// on a slow mobile link), short enough to reap a stalled one. Deliberately a
|
|
301
346
|
/// constant, not a config knob: fine-tuning intake limits is the fronting
|
|
302
|
-
/// proxy's job; the actual hazard was having no default at all.
|
|
347
|
+
/// proxy's job; the actual hazard was having no default at all. Guards the
|
|
348
|
+
/// HTTP/1 side only: h2 intake is bounded by the TLS-handshake timeout and
|
|
349
|
+
/// the h2 codec's own SETTINGS handling.
|
|
303
350
|
const HEADER_READ_TIMEOUT: Duration = Duration::from_secs(15);
|
|
304
351
|
|
|
352
|
+
/// The connection builder every data-plane connection is served with,
|
|
353
|
+
/// in both accept topologies. `http2: true` is the protocol-auto shape:
|
|
354
|
+
/// ALPN-negotiated h2 over TLS, prior-knowledge h2c on plaintext (the
|
|
355
|
+
/// builder sniffs the 24-byte preface once per connection), HTTP/1.x
|
|
356
|
+
/// for everything else. `http2: false` pins the HTTP/1 codec: no sniff,
|
|
357
|
+
/// same wire behavior as a server built without h2.
|
|
358
|
+
///
|
|
359
|
+
/// No auto Date header on either protocol: it costs a clock read per
|
|
360
|
+
/// response (together with timer reads, ~7% of tokio-side cycles in the
|
|
361
|
+
/// profile); it's a SHOULD not a MUST, and apps that need it can set it
|
|
362
|
+
/// themselves.
|
|
363
|
+
///
|
|
364
|
+
/// The http1 timer is installed so header_read_timeout actually fires:
|
|
365
|
+
/// hyper's slow-header guard is inert without one. It arms only while
|
|
366
|
+
/// the request head is being read, so it adds no per-response cost on
|
|
367
|
+
/// the hot path. The h2 side gets a timer too so its own timed
|
|
368
|
+
/// machinery (keep-alive, shutdown deadlines) can fire if ever enabled.
|
|
369
|
+
/// SETTINGS_MAX_CONCURRENT_STREAMS, derived from worker-slot capacity
|
|
370
|
+
/// (workers × threads) instead of hyper's flat 200. Two jobs: an
|
|
371
|
+
/// h2-aware balancer sees the server's real admission and spreads
|
|
372
|
+
/// streams across upstream connections instead of queueing blind, and a
|
|
373
|
+
/// hostile client can't multiply one connection into 200 queued
|
|
374
|
+
/// requests. The floor keeps tiny topologies browser-friendly (one
|
|
375
|
+
/// page's fetches still parallelize; excess streams just queue), the
|
|
376
|
+
/// cap bounds per-connection bookkeeping, and an unknown topology (an
|
|
377
|
+
/// embedder passing zeros) keeps hyper's default.
|
|
378
|
+
fn advertised_streams(workers: usize, threads: usize) -> u32 {
|
|
379
|
+
let slots = workers.saturating_mul(threads);
|
|
380
|
+
if slots == 0 {
|
|
381
|
+
return 200;
|
|
382
|
+
}
|
|
383
|
+
slots.clamp(8, 1024) as u32
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
fn conn_builder(http2: bool, max_streams: u32) -> auto::Builder<TokioExecutor> {
|
|
387
|
+
let mut builder = auto::Builder::new(TokioExecutor::new());
|
|
388
|
+
builder
|
|
389
|
+
.http1()
|
|
390
|
+
.timer(TokioTimer::new())
|
|
391
|
+
.header_read_timeout(HEADER_READ_TIMEOUT)
|
|
392
|
+
.auto_date_header(false);
|
|
393
|
+
// Beyond the stream cap, hyper's h2 defaults (16 KB frames, 1 MB
|
|
394
|
+
// windows) stay: a knob sweep (frame size 64K/256K, adaptive
|
|
395
|
+
// windows, 4/8 MB windows) moved the upload lane nowhere or slightly
|
|
396
|
+
// down once read_body coalesced its channel drain: the crossings
|
|
397
|
+
// were the cost, not the codec. The codec's abuse bounds also ship
|
|
398
|
+
// as defaults: 16 KB header lists, 20 pending remote resets then
|
|
399
|
+
// GOAWAY (rapid reset), reset-churn and empty-frame budgets.
|
|
400
|
+
builder
|
|
401
|
+
.http2()
|
|
402
|
+
.timer(TokioTimer::new())
|
|
403
|
+
.auto_date_header(false)
|
|
404
|
+
.max_concurrent_streams(max_streams);
|
|
405
|
+
if http2 {
|
|
406
|
+
builder
|
|
407
|
+
} else {
|
|
408
|
+
builder.http1_only()
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
305
412
|
async fn serve_connection<I>(
|
|
306
413
|
io: I,
|
|
307
414
|
server: Arc<ServerInner>,
|
|
@@ -310,21 +417,38 @@ async fn serve_connection<I>(
|
|
|
310
417
|
) where
|
|
311
418
|
I: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
|
|
312
419
|
{
|
|
420
|
+
let http2 = server.http2;
|
|
421
|
+
let max_streams = advertised_streams(server.topology.workers, server.topology.threads);
|
|
422
|
+
let mut drain = server.shutdown_tx.subscribe();
|
|
313
423
|
let service =
|
|
314
424
|
service_fn(move |req| handle_request(server.clone(), remote_addr, local_addr, req));
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
.
|
|
327
|
-
|
|
425
|
+
let builder = conn_builder(http2, max_streams);
|
|
426
|
+
let conn = builder.serve_connection(TokioIo::new(io), service);
|
|
427
|
+
let mut conn = std::pin::pin!(conn);
|
|
428
|
+
// Serve until done, or switch to graceful shutdown when the drain
|
|
429
|
+
// signal fires (stop_accepting): the in-flight request finishes and
|
|
430
|
+
// the connection then closes (`Connection: close` on HTTP/1,
|
|
431
|
+
// GOAWAY on h2), so a balancer moves on instead of feeding a
|
|
432
|
+
// draining server, and teardown never cuts a response mid-stream.
|
|
433
|
+
// A connection that outlives the drain deadline is still cut by the
|
|
434
|
+
// runtime teardown, as before.
|
|
435
|
+
tokio::select! {
|
|
436
|
+
_ = conn.as_mut() => {}
|
|
437
|
+
_ = drain_signal(&mut drain) => {
|
|
438
|
+
conn.as_mut().graceful_shutdown();
|
|
439
|
+
let _ = conn.as_mut().await;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/// Resolve when the server starts draining. A connection accepted just
|
|
445
|
+
/// before the signal may subscribe just after it, and `changed()` alone
|
|
446
|
+
/// would miss that edge, so the current value is checked first; a
|
|
447
|
+
/// dropped sender (teardown) counts as draining too.
|
|
448
|
+
async fn drain_signal(rx: &mut tokio::sync::watch::Receiver<bool>) {
|
|
449
|
+
if !*rx.borrow_and_update() {
|
|
450
|
+
let _ = rx.changed().await;
|
|
451
|
+
}
|
|
328
452
|
}
|
|
329
453
|
|
|
330
454
|
/// The 503 every rejection path returns; counted for stats. Branding
|
|
@@ -380,7 +504,11 @@ async fn handle_request(
|
|
|
380
504
|
None => parts.uri.path().to_string(),
|
|
381
505
|
};
|
|
382
506
|
let method = parts.method.to_string();
|
|
383
|
-
log.write_line(crate::access_log::arrival(
|
|
507
|
+
log.write_line(crate::access_log::arrival(
|
|
508
|
+
&method,
|
|
509
|
+
&target,
|
|
510
|
+
remote_addr.ip(),
|
|
511
|
+
));
|
|
384
512
|
(std::time::Instant::now(), method, target)
|
|
385
513
|
});
|
|
386
514
|
|
|
@@ -392,18 +520,18 @@ async fn handle_request(
|
|
|
392
520
|
let oversize =
|
|
393
521
|
max_body > 0 && content_length(&parts.headers).is_some_and(|len| len > max_body as u64);
|
|
394
522
|
|
|
523
|
+
let (head_tx, head_rx) = tokio::sync::oneshot::channel();
|
|
524
|
+
let responder = Arc::new(Responder::new(head_tx));
|
|
525
|
+
|
|
395
526
|
// Stream the request body through a bounded channel: hyper is polled
|
|
396
527
|
// only as fast as the Ruby side consumes (inbound backpressure), and the
|
|
397
528
|
// forwarder dropping the sender is EOF. Bodyless requests (most GETs)
|
|
398
|
-
// skip the forwarder task entirely:
|
|
399
|
-
let
|
|
400
|
-
|
|
401
|
-
let body_timeout = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
402
|
-
if oversize || hyper::body::Body::is_end_stream(&body) {
|
|
403
|
-
drop(body_tx);
|
|
529
|
+
// skip the channel and the forwarder task entirely: no channel IS the EOF.
|
|
530
|
+
let body_rx = if oversize || hyper::body::Body::is_end_stream(&body) {
|
|
531
|
+
None
|
|
404
532
|
} else {
|
|
405
|
-
let
|
|
406
|
-
let
|
|
533
|
+
let (body_tx, body_rx) = flume::bounded::<bytes::Bytes>(8);
|
|
534
|
+
let responder = responder.clone();
|
|
407
535
|
tokio::spawn(async move {
|
|
408
536
|
let mut body = body;
|
|
409
537
|
let mut total: u64 = 0;
|
|
@@ -416,16 +544,19 @@ async fn handle_request(
|
|
|
416
544
|
Ok(Some(Ok(frame))) => frame,
|
|
417
545
|
Ok(Some(Err(_))) | Ok(None) => break, // body error or clean EOF
|
|
418
546
|
Err(_) => {
|
|
419
|
-
|
|
547
|
+
responder.abandon_body(crate::response::BodyAbandon::TimedOut);
|
|
420
548
|
break;
|
|
421
549
|
}
|
|
422
550
|
};
|
|
551
|
+
// Non-data frames (h2 trailers) are dropped by design:
|
|
552
|
+
// Rack has no trailer surface. DATA frames around them
|
|
553
|
+
// still forward, and hyper reports EOF right after.
|
|
423
554
|
if let Ok(data) = frame.into_data() {
|
|
424
555
|
total += data.len() as u64;
|
|
425
556
|
if max_body > 0 && total > max_body as u64 {
|
|
426
557
|
// Past the cap: flag it and stop pulling. Dropping the
|
|
427
558
|
// sender unblocks read_body, which then raises.
|
|
428
|
-
|
|
559
|
+
responder.abandon_body(crate::response::BodyAbandon::Oversize);
|
|
429
560
|
break;
|
|
430
561
|
}
|
|
431
562
|
if body_tx.send_async(data).await.is_err() {
|
|
@@ -434,10 +565,9 @@ async fn handle_request(
|
|
|
434
565
|
}
|
|
435
566
|
}
|
|
436
567
|
});
|
|
437
|
-
|
|
568
|
+
Some(body_rx)
|
|
569
|
+
};
|
|
438
570
|
|
|
439
|
-
let (head_tx, head_rx) = tokio::sync::oneshot::channel();
|
|
440
|
-
let responder = Arc::new(Responder::new(head_tx));
|
|
441
571
|
let now = std::time::Instant::now();
|
|
442
572
|
let ctx = Box::new(RequestCtx {
|
|
443
573
|
method: parts.method,
|
|
@@ -448,8 +578,6 @@ async fn handle_request(
|
|
|
448
578
|
local_addr,
|
|
449
579
|
https: server.https,
|
|
450
580
|
body_rx,
|
|
451
|
-
body_overflow,
|
|
452
|
-
body_timeout,
|
|
453
581
|
leftover: None,
|
|
454
582
|
slot: None,
|
|
455
583
|
pin_slab: server.pin_slab.clone(),
|
|
@@ -627,7 +755,9 @@ pub fn register_worker(ruby: &Ruby, server_id: u64) -> Result<usize, Error> {
|
|
|
627
755
|
|
|
628
756
|
pub fn stop_accepting(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
629
757
|
if let Some(server) = registry::try_get(server_id) {
|
|
630
|
-
server
|
|
758
|
+
server
|
|
759
|
+
.state
|
|
760
|
+
.store(registry::STATE_DRAINING, Ordering::Relaxed);
|
|
631
761
|
let _ = server.shutdown_tx.send(true);
|
|
632
762
|
}
|
|
633
763
|
Ok(())
|
|
@@ -716,9 +846,8 @@ pub fn interrupt_all_workers(_ruby: &Ruby, server_id: u64) -> Result<(), Error>
|
|
|
716
846
|
|
|
717
847
|
pub fn shutdown_runtime(_ruby: &Ruby, server_id: u64, timeout_ms: u64) -> Result<(), Error> {
|
|
718
848
|
if let Some(server) = registry::remove(server_id) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
849
|
+
let _ = server.shutdown_tx.send(true);
|
|
850
|
+
std::mem::take(&mut *server.runtime.lock()).shutdown(Duration::from_millis(timeout_ms));
|
|
722
851
|
// The listener is closed with the runtime; its socket file is not.
|
|
723
852
|
if let Some(path) = &server.unix_path {
|
|
724
853
|
crate::listen::cleanup_unix(path);
|
|
@@ -775,10 +904,7 @@ pub type WorkerStatRow = (usize, u64, usize, u64, bool);
|
|
|
775
904
|
|
|
776
905
|
/// Per-slot rows for Server#stats parity: [index, served, in_flight,
|
|
777
906
|
/// busy_ms, quarantined] each. Empty when the server is gone.
|
|
778
|
-
pub fn worker_stats(
|
|
779
|
-
_ruby: &Ruby,
|
|
780
|
-
server_id: u64,
|
|
781
|
-
) -> Result<Vec<WorkerStatRow>, Error> {
|
|
907
|
+
pub fn worker_stats(_ruby: &Ruby, server_id: u64) -> Result<Vec<WorkerStatRow>, Error> {
|
|
782
908
|
let Some(server) = registry::try_get(server_id) else {
|
|
783
909
|
return Ok(Vec::new());
|
|
784
910
|
};
|
|
@@ -800,7 +926,9 @@ pub fn quarantine_slot(ruby: &Ruby, server_id: u64, worker_id: usize) -> Result<
|
|
|
800
926
|
/// One replacement spawned by the quarantine monitor.
|
|
801
927
|
pub fn record_quarantine_replacement(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
802
928
|
if let Some(server) = registry::try_get(server_id) {
|
|
803
|
-
server
|
|
929
|
+
server
|
|
930
|
+
.quarantine_replacements
|
|
931
|
+
.fetch_add(1, Ordering::Relaxed);
|
|
804
932
|
}
|
|
805
933
|
Ok(())
|
|
806
934
|
}
|
|
@@ -811,6 +939,539 @@ mod tests {
|
|
|
811
939
|
use crate::registry::{test_server, LANE_DEPTH};
|
|
812
940
|
use crate::request::test_ctx;
|
|
813
941
|
|
|
942
|
+
/// Serve one real connection through the production `serve_connection`
|
|
943
|
+
/// over an in-memory pipe, against a queue the test consumes itself (a
|
|
944
|
+
/// stand-in for the Ruby worker). Returns the client end.
|
|
945
|
+
fn spawn_conn(server: Arc<ServerInner>) -> tokio::io::DuplexStream {
|
|
946
|
+
let (client_io, server_io) = tokio::io::duplex(64 * 1024);
|
|
947
|
+
tokio::spawn(serve_connection(
|
|
948
|
+
server_io,
|
|
949
|
+
server,
|
|
950
|
+
"127.0.0.1:40000".parse().expect("static addr"),
|
|
951
|
+
"127.0.0.1:9292".parse().expect("static addr"),
|
|
952
|
+
));
|
|
953
|
+
client_io
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async fn take_ctx(server: &Arc<ServerInner>) -> BoxedCtx {
|
|
957
|
+
server.req_rx.recv_async().await.expect("a queued request")
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/// Read the request body the way a worker does: leftover first, then
|
|
961
|
+
/// the forwarder channel until the sender drops (EOF).
|
|
962
|
+
async fn drain_body(ctx: &mut RequestCtx) -> Vec<u8> {
|
|
963
|
+
let mut out = Vec::new();
|
|
964
|
+
if let Some(leftover) = ctx.leftover.take() {
|
|
965
|
+
out.extend_from_slice(&leftover);
|
|
966
|
+
}
|
|
967
|
+
if let Some(rx) = &ctx.body_rx {
|
|
968
|
+
while let Ok(chunk) = rx.recv_async().await {
|
|
969
|
+
out.extend_from_slice(&chunk);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
out
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
#[tokio::test]
|
|
976
|
+
async fn h2c_prior_knowledge_reaches_the_queue_as_http2() {
|
|
977
|
+
use http_body_util::{BodyExt, Full};
|
|
978
|
+
|
|
979
|
+
let server = test_server(false, 4);
|
|
980
|
+
let client_io = spawn_conn(server.clone());
|
|
981
|
+
|
|
982
|
+
let worker = tokio::spawn(async move {
|
|
983
|
+
let ctx = take_ctx(&server).await;
|
|
984
|
+
assert_eq!(ctx.version, http::Version::HTTP_2);
|
|
985
|
+
// The :authority pseudo-header arrives in the URI, where
|
|
986
|
+
// build_env picks it up; no Host header exists on h2.
|
|
987
|
+
assert_eq!(
|
|
988
|
+
ctx.uri.authority().map(|a| a.as_str()),
|
|
989
|
+
Some("kino.test:8443")
|
|
990
|
+
);
|
|
991
|
+
assert!(ctx.headers.get(http::header::HOST).is_none());
|
|
992
|
+
assert!(ctx.responder.send_response(plain_response(200, "ok\n")));
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
let (mut sender, conn) = hyper::client::conn::http2::handshake(
|
|
996
|
+
hyper_util::rt::TokioExecutor::new(),
|
|
997
|
+
TokioIo::new(client_io),
|
|
998
|
+
)
|
|
999
|
+
.await
|
|
1000
|
+
.expect("h2c prior-knowledge handshake");
|
|
1001
|
+
tokio::spawn(conn);
|
|
1002
|
+
let request = hyper::Request::builder()
|
|
1003
|
+
.uri("http://kino.test:8443/")
|
|
1004
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1005
|
+
.expect("request");
|
|
1006
|
+
let response = sender.send_request(request).await.expect("h2 response");
|
|
1007
|
+
assert_eq!(response.status(), 200);
|
|
1008
|
+
assert_eq!(
|
|
1009
|
+
response.headers().get("server").expect("branded"),
|
|
1010
|
+
"Kino",
|
|
1011
|
+
"responses stay branded over h2"
|
|
1012
|
+
);
|
|
1013
|
+
let body = response.into_body().collect().await.expect("body");
|
|
1014
|
+
assert_eq!(&body.to_bytes()[..], b"ok\n");
|
|
1015
|
+
worker.await.expect("worker assertions");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
#[tokio::test]
|
|
1019
|
+
async fn h1_is_still_served_by_the_auto_builder() {
|
|
1020
|
+
use http_body_util::{BodyExt, Full};
|
|
1021
|
+
|
|
1022
|
+
let server = test_server(false, 4);
|
|
1023
|
+
let client_io = spawn_conn(server.clone());
|
|
1024
|
+
|
|
1025
|
+
let worker = tokio::spawn(async move {
|
|
1026
|
+
let ctx = take_ctx(&server).await;
|
|
1027
|
+
assert_eq!(ctx.version, http::Version::HTTP_11);
|
|
1028
|
+
assert!(ctx.responder.send_response(plain_response(200, "h1\n")));
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(client_io))
|
|
1032
|
+
.await
|
|
1033
|
+
.expect("h1 handshake");
|
|
1034
|
+
tokio::spawn(conn);
|
|
1035
|
+
let request = hyper::Request::builder()
|
|
1036
|
+
.uri("/")
|
|
1037
|
+
.header("host", "kino.test")
|
|
1038
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1039
|
+
.expect("request");
|
|
1040
|
+
let response = sender.send_request(request).await.expect("h1 response");
|
|
1041
|
+
assert_eq!(response.status(), 200);
|
|
1042
|
+
let body = response.into_body().collect().await.expect("body");
|
|
1043
|
+
assert_eq!(&body.to_bytes()[..], b"h1\n");
|
|
1044
|
+
worker.await.expect("worker assertions");
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
#[tokio::test]
|
|
1048
|
+
async fn http2_off_refuses_the_preface_but_serves_h1() {
|
|
1049
|
+
use http_body_util::{BodyExt, Full};
|
|
1050
|
+
use hyper::service::service_fn;
|
|
1051
|
+
|
|
1052
|
+
// Driven at the builder seam: conn_builder(false) is what a
|
|
1053
|
+
// ServerInner with http2 off serves every connection with.
|
|
1054
|
+
let stub = || {
|
|
1055
|
+
service_fn(|_req: hyper::Request<hyper::body::Incoming>| async {
|
|
1056
|
+
Ok::<_, std::convert::Infallible>(plain_response(200, "pinned\n"))
|
|
1057
|
+
})
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
// An h2 prior-knowledge client must fail: the pinned h1 codec
|
|
1061
|
+
// reads the preface as a malformed request line.
|
|
1062
|
+
let (client_io, server_io) = tokio::io::duplex(64 * 1024);
|
|
1063
|
+
tokio::spawn(async move {
|
|
1064
|
+
let _ = conn_builder(false, 200)
|
|
1065
|
+
.serve_connection(TokioIo::new(server_io), stub())
|
|
1066
|
+
.await;
|
|
1067
|
+
});
|
|
1068
|
+
let refused = async {
|
|
1069
|
+
let (mut sender, conn) = hyper::client::conn::http2::handshake(
|
|
1070
|
+
hyper_util::rt::TokioExecutor::new(),
|
|
1071
|
+
TokioIo::new(client_io),
|
|
1072
|
+
)
|
|
1073
|
+
.await?;
|
|
1074
|
+
tokio::spawn(conn);
|
|
1075
|
+
let request = hyper::Request::builder()
|
|
1076
|
+
.uri("http://kino.test/")
|
|
1077
|
+
.body(Full::new(bytes::Bytes::new()))?;
|
|
1078
|
+
sender.send_request(request).await?;
|
|
1079
|
+
Ok::<_, Box<dyn std::error::Error>>(())
|
|
1080
|
+
}
|
|
1081
|
+
.await;
|
|
1082
|
+
assert!(refused.is_err(), "h2 must not be served when pinned to h1");
|
|
1083
|
+
|
|
1084
|
+
// The same pinned builder serves a plain h1 client.
|
|
1085
|
+
let (client_io, server_io) = tokio::io::duplex(64 * 1024);
|
|
1086
|
+
tokio::spawn(async move {
|
|
1087
|
+
let _ = conn_builder(false, 200)
|
|
1088
|
+
.serve_connection(TokioIo::new(server_io), stub())
|
|
1089
|
+
.await;
|
|
1090
|
+
});
|
|
1091
|
+
let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(client_io))
|
|
1092
|
+
.await
|
|
1093
|
+
.expect("h1 handshake");
|
|
1094
|
+
tokio::spawn(conn);
|
|
1095
|
+
let request = hyper::Request::builder()
|
|
1096
|
+
.uri("/")
|
|
1097
|
+
.header("host", "kino.test")
|
|
1098
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1099
|
+
.expect("request");
|
|
1100
|
+
let response = sender.send_request(request).await.expect("h1 response");
|
|
1101
|
+
assert_eq!(response.status(), 200);
|
|
1102
|
+
let body = response.into_body().collect().await.expect("body");
|
|
1103
|
+
assert_eq!(&body.to_bytes()[..], b"pinned\n");
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
#[tokio::test]
|
|
1107
|
+
async fn h2_upload_forwards_data_and_drops_trailers() {
|
|
1108
|
+
use http_body_util::{BodyExt, StreamBody};
|
|
1109
|
+
use hyper::body::Frame;
|
|
1110
|
+
|
|
1111
|
+
let server = test_server(false, 4);
|
|
1112
|
+
let client_io = spawn_conn(server.clone());
|
|
1113
|
+
|
|
1114
|
+
let worker = tokio::spawn(async move {
|
|
1115
|
+
let mut ctx = take_ctx(&server).await;
|
|
1116
|
+
assert_eq!(ctx.version, http::Version::HTTP_2);
|
|
1117
|
+
let body = drain_body(&mut ctx).await;
|
|
1118
|
+
assert_eq!(&body[..], b"hello world");
|
|
1119
|
+
assert!(
|
|
1120
|
+
ctx.responder.body_abandoned().is_none(),
|
|
1121
|
+
"a trailer frame must not abort the body read"
|
|
1122
|
+
);
|
|
1123
|
+
let response = hyper::Response::builder()
|
|
1124
|
+
.status(200)
|
|
1125
|
+
.body(crate::response::full_body(bytes::Bytes::from(
|
|
1126
|
+
body.len().to_string(),
|
|
1127
|
+
)))
|
|
1128
|
+
.expect("response");
|
|
1129
|
+
assert!(ctx.responder.send_response(response));
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
let (frames_tx, frames_rx) =
|
|
1133
|
+
flume::bounded::<Result<Frame<bytes::Bytes>, std::io::Error>>(4);
|
|
1134
|
+
frames_tx
|
|
1135
|
+
.send(Ok(Frame::data(bytes::Bytes::from_static(b"hello "))))
|
|
1136
|
+
.expect("frame");
|
|
1137
|
+
frames_tx
|
|
1138
|
+
.send(Ok(Frame::data(bytes::Bytes::from_static(b"world"))))
|
|
1139
|
+
.expect("frame");
|
|
1140
|
+
let mut trailers = http::HeaderMap::new();
|
|
1141
|
+
trailers.insert("x-checksum", "ignored".parse().expect("value"));
|
|
1142
|
+
frames_tx
|
|
1143
|
+
.send(Ok(Frame::trailers(trailers)))
|
|
1144
|
+
.expect("frame");
|
|
1145
|
+
drop(frames_tx); // EOS
|
|
1146
|
+
|
|
1147
|
+
let (mut sender, conn) = hyper::client::conn::http2::handshake(
|
|
1148
|
+
hyper_util::rt::TokioExecutor::new(),
|
|
1149
|
+
TokioIo::new(client_io),
|
|
1150
|
+
)
|
|
1151
|
+
.await
|
|
1152
|
+
.expect("h2 handshake");
|
|
1153
|
+
tokio::spawn(conn);
|
|
1154
|
+
let request = hyper::Request::builder()
|
|
1155
|
+
.method("POST")
|
|
1156
|
+
.uri("http://kino.test/upload")
|
|
1157
|
+
.body(StreamBody::new(frames_rx.into_stream()))
|
|
1158
|
+
.expect("request");
|
|
1159
|
+
let response = sender.send_request(request).await.expect("h2 response");
|
|
1160
|
+
assert_eq!(response.status(), 200);
|
|
1161
|
+
let body = response.into_body().collect().await.expect("body");
|
|
1162
|
+
assert_eq!(&body.to_bytes()[..], b"11");
|
|
1163
|
+
worker.await.expect("worker assertions");
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
#[tokio::test]
|
|
1167
|
+
async fn h2_streaming_response_arrives_chunked() {
|
|
1168
|
+
use http_body_util::{BodyExt, Full};
|
|
1169
|
+
|
|
1170
|
+
let server = test_server(false, 4);
|
|
1171
|
+
let client_io = spawn_conn(server.clone());
|
|
1172
|
+
|
|
1173
|
+
let worker = tokio::spawn(async move {
|
|
1174
|
+
let ctx = take_ctx(&server).await;
|
|
1175
|
+
let started = ctx
|
|
1176
|
+
.responder
|
|
1177
|
+
.send_stream_head(hyper::Response::builder().status(200))
|
|
1178
|
+
.expect("valid head");
|
|
1179
|
+
assert!(started);
|
|
1180
|
+
let frames = ctx.responder.body_sender().expect("open stream");
|
|
1181
|
+
for chunk in [&b"alpha "[..], &b"beta "[..], &b"gamma"[..]] {
|
|
1182
|
+
frames
|
|
1183
|
+
.send_async(Ok(hyper::body::Frame::data(bytes::Bytes::from_static(
|
|
1184
|
+
chunk,
|
|
1185
|
+
))))
|
|
1186
|
+
.await
|
|
1187
|
+
.expect("chunk accepted");
|
|
1188
|
+
}
|
|
1189
|
+
ctx.responder.finish_stream();
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
let (mut sender, conn) = hyper::client::conn::http2::handshake(
|
|
1193
|
+
hyper_util::rt::TokioExecutor::new(),
|
|
1194
|
+
TokioIo::new(client_io),
|
|
1195
|
+
)
|
|
1196
|
+
.await
|
|
1197
|
+
.expect("h2 handshake");
|
|
1198
|
+
tokio::spawn(conn);
|
|
1199
|
+
let request = hyper::Request::builder()
|
|
1200
|
+
.uri("http://kino.test/stream")
|
|
1201
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1202
|
+
.expect("request");
|
|
1203
|
+
let response = sender.send_request(request).await.expect("h2 response");
|
|
1204
|
+
assert_eq!(response.status(), 200);
|
|
1205
|
+
let body = response.into_body().collect().await.expect("body");
|
|
1206
|
+
assert_eq!(&body.to_bytes()[..], b"alpha beta gamma");
|
|
1207
|
+
worker.await.expect("worker assertions");
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
#[tokio::test]
|
|
1211
|
+
async fn h2_multiplexes_concurrent_streams_into_the_queue() {
|
|
1212
|
+
use http_body_util::{BodyExt, Full};
|
|
1213
|
+
|
|
1214
|
+
let server = test_server(false, 4);
|
|
1215
|
+
let client_io = spawn_conn(server.clone());
|
|
1216
|
+
|
|
1217
|
+
// Both streams must be queued before either is answered: that is
|
|
1218
|
+
// multiplexing observable at the worker boundary; and answering
|
|
1219
|
+
// them in reverse order proves stream completion is not FIFO.
|
|
1220
|
+
let worker = tokio::spawn(async move {
|
|
1221
|
+
let first = take_ctx(&server).await;
|
|
1222
|
+
let second = take_ctx(&server).await;
|
|
1223
|
+
let order = [second.uri.path().to_string(), first.uri.path().to_string()];
|
|
1224
|
+
assert!(second.responder.send_response(plain_response(200, "two\n")));
|
|
1225
|
+
assert!(first.responder.send_response(plain_response(200, "one\n")));
|
|
1226
|
+
order
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
let (mut sender, conn) = hyper::client::conn::http2::handshake(
|
|
1230
|
+
hyper_util::rt::TokioExecutor::new(),
|
|
1231
|
+
TokioIo::new(client_io),
|
|
1232
|
+
)
|
|
1233
|
+
.await
|
|
1234
|
+
.expect("h2 handshake");
|
|
1235
|
+
tokio::spawn(conn);
|
|
1236
|
+
let req = |path: &str| {
|
|
1237
|
+
hyper::Request::builder()
|
|
1238
|
+
.uri(format!("http://kino.test{path}"))
|
|
1239
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1240
|
+
.expect("request")
|
|
1241
|
+
};
|
|
1242
|
+
let (one, two) = tokio::join!(
|
|
1243
|
+
sender.send_request(req("/one")),
|
|
1244
|
+
sender.send_request(req("/two"))
|
|
1245
|
+
);
|
|
1246
|
+
let one = one.expect("first stream");
|
|
1247
|
+
let two = two.expect("second stream");
|
|
1248
|
+
assert_eq!(one.status(), 200);
|
|
1249
|
+
assert_eq!(two.status(), 200);
|
|
1250
|
+
let one = one.into_body().collect().await.expect("body").to_bytes();
|
|
1251
|
+
let two = two.into_body().collect().await.expect("body").to_bytes();
|
|
1252
|
+
assert_eq!(&one[..], b"one\n");
|
|
1253
|
+
assert_eq!(&two[..], b"two\n");
|
|
1254
|
+
let order = worker.await.expect("worker assertions");
|
|
1255
|
+
assert_eq!(order, ["/two".to_string(), "/one".to_string()]);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
#[tokio::test]
|
|
1259
|
+
async fn drain_finishes_the_in_flight_h2_stream_then_goaways() {
|
|
1260
|
+
use http_body_util::{BodyExt, Full};
|
|
1261
|
+
|
|
1262
|
+
let server = test_server(false, 4);
|
|
1263
|
+
let client_io = spawn_conn(server.clone());
|
|
1264
|
+
|
|
1265
|
+
let (sender, conn) = hyper::client::conn::http2::handshake(
|
|
1266
|
+
hyper_util::rt::TokioExecutor::new(),
|
|
1267
|
+
TokioIo::new(client_io),
|
|
1268
|
+
)
|
|
1269
|
+
.await
|
|
1270
|
+
.expect("h2 handshake");
|
|
1271
|
+
tokio::spawn(conn);
|
|
1272
|
+
|
|
1273
|
+
let mut in_flight_sender = sender.clone();
|
|
1274
|
+
let in_flight = tokio::spawn(async move {
|
|
1275
|
+
let request = hyper::Request::builder()
|
|
1276
|
+
.uri("http://kino.test/inflight")
|
|
1277
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1278
|
+
.expect("request");
|
|
1279
|
+
in_flight_sender.send_request(request).await
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
// The request is with the worker when the drain fires; its
|
|
1283
|
+
// response must still reach the client through the shutdown.
|
|
1284
|
+
let ctx = take_ctx(&server).await;
|
|
1285
|
+
let _ = server.shutdown_tx.send(true);
|
|
1286
|
+
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
1287
|
+
assert!(ctx.responder.send_response(plain_response(200, "late\n")));
|
|
1288
|
+
|
|
1289
|
+
let response = in_flight.await.expect("join").expect("in-flight served");
|
|
1290
|
+
assert_eq!(response.status(), 200);
|
|
1291
|
+
let body = response.into_body().collect().await.expect("body");
|
|
1292
|
+
assert_eq!(&body.to_bytes()[..], b"late\n");
|
|
1293
|
+
|
|
1294
|
+
// The connection is now GOAWAY'd: a new stream must fail.
|
|
1295
|
+
let mut sender = sender;
|
|
1296
|
+
let request = hyper::Request::builder()
|
|
1297
|
+
.uri("http://kino.test/after")
|
|
1298
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1299
|
+
.expect("request");
|
|
1300
|
+
assert!(
|
|
1301
|
+
sender.send_request(request).await.is_err(),
|
|
1302
|
+
"a drained connection must not accept new streams"
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
#[tokio::test]
|
|
1307
|
+
async fn drain_closes_an_h1_connection_after_its_response() {
|
|
1308
|
+
use http_body_util::Full;
|
|
1309
|
+
|
|
1310
|
+
let server = test_server(false, 4);
|
|
1311
|
+
let client_io = spawn_conn(server.clone());
|
|
1312
|
+
|
|
1313
|
+
let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(client_io))
|
|
1314
|
+
.await
|
|
1315
|
+
.expect("h1 handshake");
|
|
1316
|
+
tokio::spawn(conn);
|
|
1317
|
+
|
|
1318
|
+
// h1's SendRequest is not Clone: one task drives both requests;
|
|
1319
|
+
// the in-flight one, then (once it completed, so the drain has
|
|
1320
|
+
// been seen) the keep-alive follow-up that must be refused.
|
|
1321
|
+
let req = |path: &str| {
|
|
1322
|
+
hyper::Request::builder()
|
|
1323
|
+
.uri(path.to_string())
|
|
1324
|
+
.header("host", "kino.test")
|
|
1325
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1326
|
+
.expect("request")
|
|
1327
|
+
};
|
|
1328
|
+
let client = tokio::spawn(async move {
|
|
1329
|
+
let first = sender.send_request(req("/inflight")).await;
|
|
1330
|
+
let second_failed = sender.send_request(req("/after")).await.is_err();
|
|
1331
|
+
(first, second_failed)
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
let ctx = take_ctx(&server).await;
|
|
1335
|
+
let _ = server.shutdown_tx.send(true);
|
|
1336
|
+
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
1337
|
+
assert!(ctx.responder.send_response(plain_response(200, "late\n")));
|
|
1338
|
+
|
|
1339
|
+
let (first, second_failed) = client.await.expect("join");
|
|
1340
|
+
assert_eq!(first.expect("in-flight served").status(), 200);
|
|
1341
|
+
assert!(
|
|
1342
|
+
second_failed,
|
|
1343
|
+
"a drained keep-alive connection must close after its response"
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
#[test]
|
|
1348
|
+
fn advertised_streams_tracks_slot_capacity() {
|
|
1349
|
+
// Slot capacity, floored for tiny topologies and capped.
|
|
1350
|
+
assert_eq!(advertised_streams(8, 3), 24);
|
|
1351
|
+
assert_eq!(advertised_streams(2, 1), 8, "floor");
|
|
1352
|
+
assert_eq!(advertised_streams(64, 32), 1024, "cap");
|
|
1353
|
+
// Unknown topology (embedder passing zeros): hyper's default.
|
|
1354
|
+
assert_eq!(advertised_streams(0, 0), 200);
|
|
1355
|
+
assert_eq!(advertised_streams(8, 0), 200);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
#[tokio::test]
|
|
1359
|
+
async fn streams_beyond_the_advertised_cap_queue_instead_of_failing() {
|
|
1360
|
+
use http_body_util::Full;
|
|
1361
|
+
use hyper::service::service_fn;
|
|
1362
|
+
|
|
1363
|
+
// Cap of 2: six concurrent requests must all complete; the h2
|
|
1364
|
+
// client holds excess streams locally until the server frees a
|
|
1365
|
+
// slot; nothing is refused or reset.
|
|
1366
|
+
let (client_io, server_io) = tokio::io::duplex(64 * 1024);
|
|
1367
|
+
tokio::spawn(async move {
|
|
1368
|
+
let stub = service_fn(|_req: hyper::Request<hyper::body::Incoming>| async {
|
|
1369
|
+
Ok::<_, std::convert::Infallible>(plain_response(200, "capped\n"))
|
|
1370
|
+
});
|
|
1371
|
+
let _ = conn_builder(true, 2)
|
|
1372
|
+
.serve_connection(TokioIo::new(server_io), stub)
|
|
1373
|
+
.await;
|
|
1374
|
+
});
|
|
1375
|
+
let (mut sender, conn) = hyper::client::conn::http2::handshake(
|
|
1376
|
+
hyper_util::rt::TokioExecutor::new(),
|
|
1377
|
+
TokioIo::new(client_io),
|
|
1378
|
+
)
|
|
1379
|
+
.await
|
|
1380
|
+
.expect("h2 handshake");
|
|
1381
|
+
tokio::spawn(conn);
|
|
1382
|
+
let mut requests = Vec::new();
|
|
1383
|
+
for i in 0..6 {
|
|
1384
|
+
let request = hyper::Request::builder()
|
|
1385
|
+
.uri(format!("http://kino.test/{i}"))
|
|
1386
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1387
|
+
.expect("request");
|
|
1388
|
+
requests.push(sender.send_request(request));
|
|
1389
|
+
}
|
|
1390
|
+
for request in requests {
|
|
1391
|
+
let response = request.await.expect("queued stream served");
|
|
1392
|
+
assert_eq!(response.status(), 200);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
#[tokio::test]
|
|
1397
|
+
async fn rapid_stream_resets_do_not_wedge_the_pipeline() {
|
|
1398
|
+
use http_body_util::{BodyExt, Full};
|
|
1399
|
+
|
|
1400
|
+
let server = test_server(false, 64);
|
|
1401
|
+
let client_io = spawn_conn(server.clone());
|
|
1402
|
+
|
|
1403
|
+
// A worker that answers everything it sees until told to stop;
|
|
1404
|
+
// answers to already-reset streams just vanish, as in production.
|
|
1405
|
+
let worker = tokio::spawn(async move {
|
|
1406
|
+
loop {
|
|
1407
|
+
let Ok(ctx) = server.req_rx.recv_async().await else {
|
|
1408
|
+
break;
|
|
1409
|
+
};
|
|
1410
|
+
let done = ctx.uri.path() == "/done";
|
|
1411
|
+
ctx.responder.send_response(plain_response(200, "ok\n"));
|
|
1412
|
+
if done {
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
let (mut sender, conn) = hyper::client::conn::http2::handshake(
|
|
1419
|
+
hyper_util::rt::TokioExecutor::new(),
|
|
1420
|
+
TokioIo::new(client_io),
|
|
1421
|
+
)
|
|
1422
|
+
.await
|
|
1423
|
+
.expect("h2 handshake");
|
|
1424
|
+
tokio::spawn(conn);
|
|
1425
|
+
|
|
1426
|
+
// Fire-and-cancel: dropping the response future resets the
|
|
1427
|
+
// stream. The codec bounds reset churn; the server must keep
|
|
1428
|
+
// serving afterwards.
|
|
1429
|
+
for i in 0..40 {
|
|
1430
|
+
let request = hyper::Request::builder()
|
|
1431
|
+
.uri(format!("http://kino.test/cancel/{i}"))
|
|
1432
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1433
|
+
.expect("request");
|
|
1434
|
+
drop(sender.send_request(request));
|
|
1435
|
+
}
|
|
1436
|
+
let request = hyper::Request::builder()
|
|
1437
|
+
.uri("http://kino.test/done")
|
|
1438
|
+
.body(Full::new(bytes::Bytes::new()))
|
|
1439
|
+
.expect("request");
|
|
1440
|
+
let response = sender.send_request(request).await.expect("still served");
|
|
1441
|
+
assert_eq!(response.status(), 200);
|
|
1442
|
+
let body = response.into_body().collect().await.expect("body");
|
|
1443
|
+
assert_eq!(&body.to_bytes()[..], b"ok\n");
|
|
1444
|
+
worker.await.expect("worker loop");
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
#[tokio::test(start_paused = true)]
|
|
1448
|
+
async fn header_read_timeout_still_fires_through_the_auto_builder() {
|
|
1449
|
+
use hyper::service::service_fn;
|
|
1450
|
+
use tokio::io::AsyncWriteExt;
|
|
1451
|
+
|
|
1452
|
+
let (mut client_io, server_io) = tokio::io::duplex(64 * 1024);
|
|
1453
|
+
let conn = tokio::spawn(async move {
|
|
1454
|
+
let _ = conn_builder(true, 200)
|
|
1455
|
+
.serve_connection(
|
|
1456
|
+
TokioIo::new(server_io),
|
|
1457
|
+
service_fn(|_req: hyper::Request<hyper::body::Incoming>| async {
|
|
1458
|
+
Ok::<_, std::convert::Infallible>(plain_response(200, "never\n"))
|
|
1459
|
+
}),
|
|
1460
|
+
)
|
|
1461
|
+
.await;
|
|
1462
|
+
});
|
|
1463
|
+
// A partial h1 request line, then silence: the slow-header guard
|
|
1464
|
+
// must reap the connection (paused clock auto-advances past 15s).
|
|
1465
|
+
client_io
|
|
1466
|
+
.write_all(b"GET / HT")
|
|
1467
|
+
.await
|
|
1468
|
+
.expect("partial write");
|
|
1469
|
+
tokio::time::timeout(Duration::from_secs(60), conn)
|
|
1470
|
+
.await
|
|
1471
|
+
.expect("connection reaped by header_read_timeout")
|
|
1472
|
+
.expect("serve task join");
|
|
1473
|
+
}
|
|
1474
|
+
|
|
814
1475
|
#[test]
|
|
815
1476
|
fn dispatch_with_no_slots_reports_full() {
|
|
816
1477
|
let server = test_server(true, 4);
|
|
@@ -851,7 +1512,10 @@ mod tests {
|
|
|
851
1512
|
server.register_worker();
|
|
852
1513
|
server.slots.read()[0].lane_tx.lock().take();
|
|
853
1514
|
|
|
854
|
-
assert!(matches!(
|
|
1515
|
+
assert!(matches!(
|
|
1516
|
+
try_dispatch(&server, test_ctx()),
|
|
1517
|
+
Dispatch::Closed
|
|
1518
|
+
));
|
|
855
1519
|
}
|
|
856
1520
|
|
|
857
1521
|
#[test]
|
|
@@ -894,9 +1558,7 @@ mod tests {
|
|
|
894
1558
|
let server = test_server(true, 4);
|
|
895
1559
|
server.register_worker();
|
|
896
1560
|
server.register_worker();
|
|
897
|
-
server.slots.read()[0]
|
|
898
|
-
.parked
|
|
899
|
-
.store(true, Ordering::Relaxed);
|
|
1561
|
+
server.slots.read()[0].parked.store(true, Ordering::Relaxed);
|
|
900
1562
|
|
|
901
1563
|
// Both dispatches land on the awake lane (slot 1), regardless of
|
|
902
1564
|
// where the rotating cursor starts.
|