kino 0.5.0 → 0.7.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 +67 -0
- data/Cargo.lock +92 -1
- data/README.md +153 -44
- data/doc/architecture.md +64 -3
- data/doc/benchmarks.md +120 -26
- data/ext/kino/Cargo.toml +8 -3
- data/ext/kino/src/control.rs +103 -6
- data/ext/kino/src/env_strings.rs +350 -69
- data/ext/kino/src/lib.rs +22 -7
- data/ext/kino/src/pin.rs +21 -9
- data/ext/kino/src/queue.rs +158 -46
- data/ext/kino/src/registry.rs +145 -0
- data/ext/kino/src/request.rs +151 -70
- data/ext/kino/src/response.rs +39 -5
- data/ext/kino/src/server.rs +864 -33
- data/ext/kino/src/test_support.rs +44 -0
- data/ext/kino/src/tls.rs +119 -9
- data/lib/kino/cli.rb +12 -4
- data/lib/kino/configuration.rb +22 -2
- data/lib/kino/monitor.rb +52 -0
- data/lib/kino/pool_scaler.rb +103 -0
- data/lib/kino/quarantine_monitor.rb +5 -37
- data/lib/kino/ractor_supervisor.rb +102 -8
- data/lib/kino/server.rb +66 -85
- data/lib/kino/slot_bank.rb +31 -0
- data/lib/kino/templates/kino.rb.tt +20 -0
- data/lib/kino/threaded_pool.rb +186 -0
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +14 -11
- data/lib/kino.rb +4 -0
- data/lib/rackup/handler/kino.rb +4 -4
- data/sig/kino.rbs +3 -0
- metadata +19 -1
data/ext/kino/src/request.rs
CHANGED
|
@@ -24,14 +24,10 @@ pub struct RequestCtx {
|
|
|
24
24
|
pub local_addr: SocketAddr,
|
|
25
25
|
pub https: bool,
|
|
26
26
|
/// Request body, streamed from hyper through a bounded channel: hyper is
|
|
27
|
-
/// only polled as Ruby consumes, so inbound backpressure is free.
|
|
28
|
-
|
|
29
|
-
///
|
|
30
|
-
|
|
31
|
-
pub body_overflow: Arc<std::sync::atomic::AtomicBool>,
|
|
32
|
-
/// Set by the body forwarder when the client stalled past the idle
|
|
33
|
-
/// deadline: the next read raises so the worker reclaims its slot.
|
|
34
|
-
pub body_timeout: Arc<std::sync::atomic::AtomicBool>,
|
|
27
|
+
/// only polled as Ruby consumes, so inbound backpressure is free. None
|
|
28
|
+
/// when no byte can ever arrive (most GETs): bodyless requests skip the
|
|
29
|
+
/// channel allocation entirely.
|
|
30
|
+
pub body_rx: Option<flume::Receiver<Bytes>>,
|
|
35
31
|
/// When a frame is bigger than read_body's max_len, the rest waits here.
|
|
36
32
|
pub leftover: Option<Bytes>,
|
|
37
33
|
/// The owning worker slot (set at admit time, queue.rs); its interrupt
|
|
@@ -142,7 +138,10 @@ pub fn build_env(ruby: &Ruby, ctx: &RequestCtx) -> Result<RHash, Error> {
|
|
|
142
138
|
use crate::env_strings;
|
|
143
139
|
|
|
144
140
|
let s = env_strings::get();
|
|
145
|
-
|
|
141
|
+
// Pre-sized: a dozen static keys plus one per header lands near 30
|
|
142
|
+
// entries, which a default Hash reaches only via the ar-to-st
|
|
143
|
+
// migration and a rehash per doubling.
|
|
144
|
+
let env = ruby.hash_new_capa(32);
|
|
146
145
|
// Opaque -> live RString for this thread's Ruby handle.
|
|
147
146
|
let live = |o: magnus::value::Opaque<RString>| ruby.get_inner(o);
|
|
148
147
|
|
|
@@ -163,6 +162,7 @@ pub fn build_env(ruby: &Ruby, ctx: &RequestCtx) -> Result<RHash, Error> {
|
|
|
163
162
|
env.aset(live(s.query_string), query)?;
|
|
164
163
|
let protocol = match ctx.version {
|
|
165
164
|
http::Version::HTTP_10 => s.http10,
|
|
165
|
+
http::Version::HTTP_2 => s.http2,
|
|
166
166
|
_ => s.http11,
|
|
167
167
|
};
|
|
168
168
|
env.aset(live(s.server_protocol), live(protocol))?;
|
|
@@ -183,37 +183,25 @@ pub fn build_env(ruby: &Ruby, ctx: &RequestCtx) -> Result<RHash, Error> {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
// SERVER_NAME/PORT: prefer the Host header (HTTP/1.1 requires it),
|
|
187
|
-
// fall back to the accepted socket's local address.
|
|
188
|
-
let host_header = ctx.headers.get(http::header::HOST);
|
|
189
|
-
match host_header {
|
|
190
|
-
Some(host) => {
|
|
191
|
-
let local_port = ctx.local_addr.port();
|
|
192
|
-
env_strings::set_host_env(ruby, env, host.as_bytes(), || {
|
|
193
|
-
match std::str::from_utf8(host.as_bytes()) {
|
|
194
|
-
Ok(h) => split_host_port(h, local_port),
|
|
195
|
-
Err(_) => (
|
|
196
|
-
String::from_utf8_lossy(host.as_bytes()).into_owned(),
|
|
197
|
-
local_port,
|
|
198
|
-
),
|
|
199
|
-
}
|
|
200
|
-
})?;
|
|
201
|
-
}
|
|
202
|
-
None => {
|
|
203
|
-
let ip = ctx.local_addr.ip();
|
|
204
|
-
let port = ctx.local_addr.port();
|
|
205
|
-
env_strings::set_host_env(ruby, env, format!("\0{ip}:{port}").as_bytes(), || {
|
|
206
|
-
(ip.to_string(), port)
|
|
207
|
-
})?;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
186
|
for name in ctx.headers.keys() {
|
|
212
|
-
|
|
187
|
+
let key = if *name == http::header::CONTENT_TYPE {
|
|
188
|
+
live(s.content_type)
|
|
189
|
+
} else if *name == http::header::CONTENT_LENGTH {
|
|
190
|
+
live(s.content_length)
|
|
191
|
+
} else {
|
|
192
|
+
match s.header_names.get(name.as_str()) {
|
|
193
|
+
Some(cached) => live(*cached),
|
|
194
|
+
None => ruby.str_new(&env_strings::cgi_name(name.as_str())),
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Single-value fast path (the overwhelming majority); the value
|
|
199
|
+
// cache turns low-cardinality repeats (UA, accept-*, sec-ch-*)
|
|
200
|
+
// into a lookup of one frozen string instead of an allocation.
|
|
213
201
|
let mut values = ctx.headers.get_all(name).iter();
|
|
214
202
|
let first = values.next().map(|v| v.as_bytes()).unwrap_or(b"");
|
|
215
|
-
|
|
216
|
-
None => ruby.
|
|
203
|
+
match values.next() {
|
|
204
|
+
None => env_strings::set_value_env(ruby, env, key, name.as_str(), first)?,
|
|
217
205
|
Some(second) => {
|
|
218
206
|
let sep: &[u8] = if name == http::header::COOKIE {
|
|
219
207
|
b"; "
|
|
@@ -230,30 +218,63 @@ pub fn build_env(ruby: &Ruby, ctx: &RequestCtx) -> Result<RHash, Error> {
|
|
|
230
218
|
joined.extend_from_slice(sep);
|
|
231
219
|
joined.extend_from_slice(v.as_bytes());
|
|
232
220
|
}
|
|
233
|
-
ruby.str_from_slice(&joined)
|
|
221
|
+
env.aset(key, ruby.str_from_slice(&joined))?;
|
|
234
222
|
}
|
|
235
|
-
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
236
225
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
226
|
+
// SERVER_NAME/SERVER_PORT/HTTP_HOST, best source first: the request
|
|
227
|
+
// URI's authority (every h2 request via :authority; also h1
|
|
228
|
+
// absolute-form, where it beats Host per RFC 9112), then the Host
|
|
229
|
+
// header (HTTP/1.1 requires it), then the accepted socket's local
|
|
230
|
+
// address. Runs after the header loop so an authority-derived
|
|
231
|
+
// HTTP_HOST wins over a literal host header a proxy forwarded.
|
|
232
|
+
if let Some(authority) = ctx.uri.authority() {
|
|
233
|
+
let local_port = ctx.local_addr.port();
|
|
234
|
+
env_strings::set_authority_env(ruby, env, authority.as_str(), || {
|
|
235
|
+
(
|
|
236
|
+
authority.host().to_string(),
|
|
237
|
+
authority.port_u16().unwrap_or(local_port),
|
|
238
|
+
)
|
|
239
|
+
})?;
|
|
240
|
+
} else {
|
|
241
|
+
match ctx.headers.get(http::header::HOST) {
|
|
242
|
+
Some(host) => {
|
|
243
|
+
let local_port = ctx.local_addr.port();
|
|
244
|
+
env_strings::set_host_env(
|
|
245
|
+
ruby,
|
|
246
|
+
env,
|
|
247
|
+
host.as_bytes(),
|
|
248
|
+
|| match std::str::from_utf8(host.as_bytes()) {
|
|
249
|
+
Ok(h) => split_host_port(h, local_port),
|
|
250
|
+
Err(_) => (
|
|
251
|
+
String::from_utf8_lossy(host.as_bytes()).into_owned(),
|
|
252
|
+
local_port,
|
|
253
|
+
),
|
|
254
|
+
},
|
|
255
|
+
)?;
|
|
245
256
|
}
|
|
246
|
-
|
|
247
|
-
|
|
257
|
+
None => {
|
|
258
|
+
let ip = ctx.local_addr.ip();
|
|
259
|
+
let port = ctx.local_addr.port();
|
|
260
|
+
env_strings::set_host_env(ruby, env, format!("\0{ip}:{port}").as_bytes(), || {
|
|
261
|
+
(ip.to_string(), port)
|
|
262
|
+
})?;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
248
265
|
}
|
|
249
266
|
|
|
250
267
|
Ok(env)
|
|
251
268
|
}
|
|
252
269
|
|
|
253
|
-
/// Bodyless requests
|
|
254
|
-
///
|
|
270
|
+
/// Bodyless requests carry no channel at all; a present channel that is
|
|
271
|
+
/// disconnected and empty (EOF already reached) also has nothing left.
|
|
255
272
|
fn body_possible(ctx: &RequestCtx) -> bool {
|
|
256
|
-
ctx.leftover.is_some()
|
|
273
|
+
ctx.leftover.is_some()
|
|
274
|
+
|| ctx
|
|
275
|
+
.body_rx
|
|
276
|
+
.as_ref()
|
|
277
|
+
.is_some_and(|rx| !(rx.is_empty() && rx.is_disconnected()))
|
|
257
278
|
}
|
|
258
279
|
|
|
259
280
|
/// Complete response in one shot: status, the Rack headers Hash (iterated
|
|
@@ -307,7 +328,9 @@ impl Request {
|
|
|
307
328
|
let mut chunk = match ctx.leftover.take() {
|
|
308
329
|
Some(bytes) => bytes,
|
|
309
330
|
None => {
|
|
310
|
-
let body_rx = ctx.body_rx.clone()
|
|
331
|
+
let Some(body_rx) = ctx.body_rx.clone() else {
|
|
332
|
+
return Ok(None); // bodyless request: EOF from the start
|
|
333
|
+
};
|
|
311
334
|
let outcome = block_on(&ctx.slot, || {
|
|
312
335
|
match body_rx.recv_timeout(crate::queue::TICK) {
|
|
313
336
|
Ok(bytes) => Some(Some(bytes)),
|
|
@@ -320,13 +343,12 @@ impl Request {
|
|
|
320
343
|
Some(None) => {
|
|
321
344
|
// Disconnected: a clean EOF, unless the forwarder
|
|
322
345
|
// abandoned the body (too large, or the client stalled).
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
return Ok(None); // EOF
|
|
346
|
+
use crate::response::BodyAbandon;
|
|
347
|
+
return match ctx.responder.body_abandoned() {
|
|
348
|
+
Some(BodyAbandon::Oversize) => Err(body_too_large_error(ruby)),
|
|
349
|
+
Some(BodyAbandon::TimedOut) => Err(body_timeout_error(ruby)),
|
|
350
|
+
None => Ok(None), // EOF
|
|
351
|
+
};
|
|
330
352
|
}
|
|
331
353
|
None => return Err(interrupted_error(ruby)),
|
|
332
354
|
}
|
|
@@ -335,8 +357,39 @@ impl Request {
|
|
|
335
357
|
|
|
336
358
|
if chunk.len() > max_len {
|
|
337
359
|
ctx.leftover = Some(chunk.split_off(max_len));
|
|
360
|
+
return Ok(Some(ruby.str_from_slice(&chunk)));
|
|
338
361
|
}
|
|
339
|
-
|
|
362
|
+
|
|
363
|
+
// Greedy drain: whatever the forwarder has already queued rides
|
|
364
|
+
// out in this same crossing, up to max_len. A body arriving as
|
|
365
|
+
// small DATA frames (HTTP/2 caps them at 16 KB unless negotiated
|
|
366
|
+
// higher) then costs one GVL round-trip and one Ruby string, not
|
|
367
|
+
// one per frame. Non-blocking: a chunk that has not arrived yet
|
|
368
|
+
// is next call's business, as are EOF and abandonment (try_recv's
|
|
369
|
+
// Disconnected included; the recv above maps them to errors).
|
|
370
|
+
let mut assembled: Option<RString> = None;
|
|
371
|
+
if let Some(body_rx) = ctx.body_rx.clone() {
|
|
372
|
+
let mut total = chunk.len();
|
|
373
|
+
while total < max_len {
|
|
374
|
+
let Ok(mut more) = body_rx.try_recv() else {
|
|
375
|
+
break;
|
|
376
|
+
};
|
|
377
|
+
if more.len() > max_len - total {
|
|
378
|
+
ctx.leftover = Some(more.split_off(max_len - total));
|
|
379
|
+
}
|
|
380
|
+
total += more.len();
|
|
381
|
+
let out = *assembled.get_or_insert_with(|| {
|
|
382
|
+
let out = ruby.str_buf_new(max_len);
|
|
383
|
+
out.cat(&chunk[..]);
|
|
384
|
+
out
|
|
385
|
+
});
|
|
386
|
+
out.cat(&more[..]);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
Ok(Some(match assembled {
|
|
390
|
+
Some(out) => out,
|
|
391
|
+
None => ruby.str_from_slice(&chunk),
|
|
392
|
+
}))
|
|
340
393
|
}
|
|
341
394
|
|
|
342
395
|
/// Start a streaming response: head goes out now, chunks follow via
|
|
@@ -439,7 +492,7 @@ fn coerce_str(value: Value) -> Result<RString, Error> {
|
|
|
439
492
|
}
|
|
440
493
|
}
|
|
441
494
|
|
|
442
|
-
fn split_host_port(host: &str, default_port: u16) -> (String, u16) {
|
|
495
|
+
pub(crate) fn split_host_port(host: &str, default_port: u16) -> (String, u16) {
|
|
443
496
|
match host.rsplit_once(':') {
|
|
444
497
|
Some((name, port)) if !name.is_empty() => match port.parse() {
|
|
445
498
|
Ok(p) => (name.to_string(), p),
|
|
@@ -453,7 +506,6 @@ fn split_host_port(host: &str, default_port: u16) -> (String, u16) {
|
|
|
453
506
|
/// fires the Drop-500 backstop into a dropped receiver, which is a no-op.
|
|
454
507
|
#[cfg(test)]
|
|
455
508
|
pub fn test_ctx() -> crate::registry::BoxedCtx {
|
|
456
|
-
let (_body_tx, body_rx) = flume::bounded(1);
|
|
457
509
|
let (head_tx, _head_rx) = tokio::sync::oneshot::channel();
|
|
458
510
|
Box::new(RequestCtx {
|
|
459
511
|
method: http::Method::GET,
|
|
@@ -463,9 +515,7 @@ pub fn test_ctx() -> crate::registry::BoxedCtx {
|
|
|
463
515
|
remote_addr: "127.0.0.1:40000".parse().expect("static addr"),
|
|
464
516
|
local_addr: "127.0.0.1:9292".parse().expect("static addr"),
|
|
465
517
|
https: false,
|
|
466
|
-
body_rx,
|
|
467
|
-
body_overflow: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
|
468
|
-
body_timeout: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
|
518
|
+
body_rx: None,
|
|
469
519
|
leftover: None,
|
|
470
520
|
slot: None,
|
|
471
521
|
pin_slab: Arc::new(crate::pin::PinSlab::new()),
|
|
@@ -511,10 +561,34 @@ mod tests {
|
|
|
511
561
|
assert_eq!(split_host_port(":8080", 80), (":8080".into(), 80));
|
|
512
562
|
}
|
|
513
563
|
|
|
564
|
+
#[test]
|
|
565
|
+
fn authority_host_port_derivation() {
|
|
566
|
+
// The same derivation build_env applies to a URI authority (the
|
|
567
|
+
// h2 :authority pseudo-header): http::uri does the split, the
|
|
568
|
+
// socket's local port fills a missing port.
|
|
569
|
+
let with_port: http::Uri = "https://example.com:8443/x".parse().expect("uri");
|
|
570
|
+
let authority = with_port.authority().expect("authority");
|
|
571
|
+
assert_eq!(authority.host(), "example.com");
|
|
572
|
+
assert_eq!(authority.port_u16(), Some(8443));
|
|
573
|
+
assert_eq!(authority.as_str(), "example.com:8443");
|
|
574
|
+
|
|
575
|
+
let bare: http::Uri = "https://example.com/x".parse().expect("uri");
|
|
576
|
+
let authority = bare.authority().expect("authority");
|
|
577
|
+
assert_eq!(authority.host(), "example.com");
|
|
578
|
+
assert_eq!(authority.port_u16().unwrap_or(9292), 9292);
|
|
579
|
+
|
|
580
|
+
// Bracketed IPv6: host() keeps the brackets, matching the h1
|
|
581
|
+
// split_host_port convention for SERVER_NAME.
|
|
582
|
+
let v6: http::Uri = "https://[::1]:8443/x".parse().expect("uri");
|
|
583
|
+
let authority = v6.authority().expect("authority");
|
|
584
|
+
assert_eq!(authority.host(), "[::1]");
|
|
585
|
+
assert_eq!(authority.port_u16(), Some(8443));
|
|
586
|
+
assert_eq!(authority.as_str(), "[::1]:8443");
|
|
587
|
+
}
|
|
588
|
+
|
|
514
589
|
#[test]
|
|
515
590
|
fn body_possible_reflects_channel_and_leftover_state() {
|
|
516
|
-
// test_ctx
|
|
517
|
-
// byte can ever arrive.
|
|
591
|
+
// test_ctx carries no channel: bodyless by construction.
|
|
518
592
|
let ctx = test_ctx();
|
|
519
593
|
assert!(!body_possible(&ctx));
|
|
520
594
|
|
|
@@ -526,16 +600,23 @@ mod tests {
|
|
|
526
600
|
// A live sender means bytes may still arrive.
|
|
527
601
|
let (body_tx, body_rx) = flume::bounded(1);
|
|
528
602
|
let mut ctx = test_ctx();
|
|
529
|
-
ctx.body_rx = body_rx;
|
|
603
|
+
ctx.body_rx = Some(body_rx);
|
|
530
604
|
assert!(body_possible(&ctx));
|
|
531
605
|
drop(body_tx);
|
|
532
606
|
|
|
607
|
+
// A dropped sender with nothing queued: EOF already reached.
|
|
608
|
+
let (body_tx, body_rx) = flume::bounded::<bytes::Bytes>(1);
|
|
609
|
+
drop(body_tx);
|
|
610
|
+
let mut ctx = test_ctx();
|
|
611
|
+
ctx.body_rx = Some(body_rx);
|
|
612
|
+
assert!(!body_possible(&ctx));
|
|
613
|
+
|
|
533
614
|
// Disconnected but with a queued chunk: still readable.
|
|
534
615
|
let (body_tx, body_rx) = flume::bounded(1);
|
|
535
616
|
body_tx.send(bytes::Bytes::from_static(b"x")).unwrap();
|
|
536
617
|
drop(body_tx);
|
|
537
618
|
let mut ctx = test_ctx();
|
|
538
|
-
ctx.body_rx = body_rx;
|
|
619
|
+
ctx.body_rx = Some(body_rx);
|
|
539
620
|
assert!(body_possible(&ctx));
|
|
540
621
|
}
|
|
541
622
|
}
|
data/ext/kino/src/response.rs
CHANGED
|
@@ -10,21 +10,35 @@
|
|
|
10
10
|
//! makes `write_chunk` block in Ruby, with the GVL released.
|
|
11
11
|
|
|
12
12
|
use std::io;
|
|
13
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
13
|
+
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
|
14
14
|
|
|
15
15
|
use bytes::Bytes;
|
|
16
|
-
use http_body_util::{
|
|
16
|
+
use http_body_util::{Either, Full, StreamBody};
|
|
17
17
|
use parking_lot::Mutex;
|
|
18
18
|
|
|
19
19
|
pub type BodyFrame = hyper::body::Frame<Bytes>;
|
|
20
20
|
pub type FrameResult = Result<BodyFrame, io::Error>;
|
|
21
|
-
|
|
21
|
+
/// Both response shapes as one concrete type: no per-response Box or
|
|
22
|
+
/// vtable, unlike BoxBody. Either erases the two error types itself.
|
|
23
|
+
pub type ResponseBody =
|
|
24
|
+
Either<Full<Bytes>, StreamBody<flume::r#async::RecvStream<'static, FrameResult>>>;
|
|
22
25
|
pub type HyperResponse = hyper::Response<ResponseBody>;
|
|
23
26
|
|
|
24
27
|
const STREAM_BUFFER: usize = 8;
|
|
25
28
|
|
|
29
|
+
/// Why the body forwarder stopped pulling a request body: read_body turns
|
|
30
|
+
/// these into raised errors instead of a (truncated) clean EOF.
|
|
31
|
+
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
32
|
+
pub enum BodyAbandon {
|
|
33
|
+
Oversize,
|
|
34
|
+
TimedOut,
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
pub struct Responder {
|
|
27
38
|
responded: AtomicBool,
|
|
39
|
+
/// The forwarder's abandonment flag rides in the one per-request shared
|
|
40
|
+
/// allocation rather than its own Arc. 0 = none, 1/2 = BodyAbandon.
|
|
41
|
+
body_abandoned: AtomicU8,
|
|
28
42
|
head_tx: Mutex<Option<tokio::sync::oneshot::Sender<HyperResponse>>>,
|
|
29
43
|
body_tx: Mutex<Option<flume::Sender<FrameResult>>>,
|
|
30
44
|
}
|
|
@@ -33,11 +47,31 @@ impl Responder {
|
|
|
33
47
|
pub fn new(head_tx: tokio::sync::oneshot::Sender<HyperResponse>) -> Self {
|
|
34
48
|
Responder {
|
|
35
49
|
responded: AtomicBool::new(false),
|
|
50
|
+
body_abandoned: AtomicU8::new(0),
|
|
36
51
|
head_tx: Mutex::new(Some(head_tx)),
|
|
37
52
|
body_tx: Mutex::new(None),
|
|
38
53
|
}
|
|
39
54
|
}
|
|
40
55
|
|
|
56
|
+
/// Record why the body forwarder gave up; the store happens before the
|
|
57
|
+
/// sender drops, so a reader that saw Disconnected sees the flag too.
|
|
58
|
+
pub fn abandon_body(&self, why: BodyAbandon) {
|
|
59
|
+
let code = match why {
|
|
60
|
+
BodyAbandon::Oversize => 1,
|
|
61
|
+
BodyAbandon::TimedOut => 2,
|
|
62
|
+
};
|
|
63
|
+
self.body_abandoned.store(code, Ordering::Relaxed);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Whether (and why) the forwarder abandoned the request body.
|
|
67
|
+
pub fn body_abandoned(&self) -> Option<BodyAbandon> {
|
|
68
|
+
match self.body_abandoned.load(Ordering::Relaxed) {
|
|
69
|
+
1 => Some(BodyAbandon::Oversize),
|
|
70
|
+
2 => Some(BodyAbandon::TimedOut),
|
|
71
|
+
_ => None,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
41
75
|
/// Claim the right to respond. First caller wins; everyone else gets None.
|
|
42
76
|
fn claim(&self) -> Option<tokio::sync::oneshot::Sender<HyperResponse>> {
|
|
43
77
|
if self.responded.swap(true, Ordering::SeqCst) {
|
|
@@ -65,7 +99,7 @@ impl Responder {
|
|
|
65
99
|
return Ok(false);
|
|
66
100
|
};
|
|
67
101
|
let (body_tx, body_rx) = flume::bounded::<FrameResult>(STREAM_BUFFER);
|
|
68
|
-
let response = builder.body(StreamBody::new(body_rx.into_stream())
|
|
102
|
+
let response = builder.body(Either::Right(StreamBody::new(body_rx.into_stream())))?;
|
|
69
103
|
*self.body_tx.lock() = Some(body_tx);
|
|
70
104
|
let _ = tx.send(response);
|
|
71
105
|
Ok(true)
|
|
@@ -98,7 +132,7 @@ impl Responder {
|
|
|
98
132
|
}
|
|
99
133
|
|
|
100
134
|
pub fn full_body(bytes: Bytes) -> ResponseBody {
|
|
101
|
-
Full::new(bytes)
|
|
135
|
+
Either::Left(Full::new(bytes))
|
|
102
136
|
}
|
|
103
137
|
|
|
104
138
|
/// Canned plain-text response, built entirely on the Rust side (used for
|