kino 0.2.1 → 0.4.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 +72 -0
- data/Cargo.lock +192 -52
- data/README.md +131 -16
- data/exe/kino +0 -4
- data/ext/kino/Cargo.toml +4 -2
- data/ext/kino/src/access_log.rs +245 -0
- data/ext/kino/src/control.rs +701 -0
- data/ext/kino/src/cpus.rs +37 -0
- data/ext/kino/src/lib.rs +22 -1
- data/ext/kino/src/listen.rs +134 -0
- data/ext/kino/src/log.rs +144 -0
- data/ext/kino/src/mono.rs +26 -0
- data/ext/kino/src/queue.rs +11 -0
- data/ext/kino/src/registry.rs +158 -0
- data/ext/kino/src/request.rs +52 -2
- data/ext/kino/src/server.rs +229 -53
- data/ext/kino/src/style.rs +42 -39
- data/lib/kino/cli.rb +23 -9
- data/lib/kino/configuration.rb +77 -5
- data/lib/kino/errors_stream.rb +4 -3
- data/lib/kino/hook_fire.rb +22 -0
- data/lib/kino/log.rb +104 -0
- data/lib/kino/quarantine_monitor.rb +73 -0
- data/lib/kino/ractor_supervisor.rb +66 -16
- data/lib/kino/server.rb +201 -34
- data/lib/kino/templates/kino.rb.tt +62 -5
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +46 -30
- data/lib/kino/worker_hooks.rb +13 -0
- data/lib/kino.rb +14 -0
- data/lib/rackup/handler/kino.rb +88 -0
- data/sig/kino.rbs +71 -1
- metadata +26 -1
data/ext/kino/src/request.rs
CHANGED
|
@@ -41,6 +41,44 @@ pub struct RequestCtx {
|
|
|
41
41
|
/// here and ride to hyper without a copy (pin.rs).
|
|
42
42
|
pub pin_slab: Arc<crate::pin::PinSlab>,
|
|
43
43
|
pub responder: Arc<Responder>,
|
|
44
|
+
/// When this request entered the queue, for the queue-wait histogram.
|
|
45
|
+
/// Stamped at ctx creation; read once at admit (queue.rs).
|
|
46
|
+
pub enqueued_at: std::time::Instant,
|
|
47
|
+
/// Whether the access log wants timing: decided at intake, read on
|
|
48
|
+
/// the way out, so an idle log costs nothing per request.
|
|
49
|
+
pub timed: bool,
|
|
50
|
+
/// Queue wait, stamped at admit (queue.rs): the log's `wait`.
|
|
51
|
+
pub wait: std::time::Duration,
|
|
52
|
+
/// When a worker took the request; elapsed at the response head it is
|
|
53
|
+
/// the log's `ruby`.
|
|
54
|
+
pub admitted_at: std::time::Instant,
|
|
55
|
+
/// GC pause and objects allocated during the app call, when the
|
|
56
|
+
/// worker measured them (Request#timing).
|
|
57
|
+
pub gc: Option<(std::time::Duration, u64)>,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl RequestCtx {
|
|
61
|
+
/// The timing this request carries to the access log.
|
|
62
|
+
fn timing(&self) -> crate::access_log::Timing {
|
|
63
|
+
crate::access_log::Timing {
|
|
64
|
+
wait: self.wait,
|
|
65
|
+
ruby: self.admitted_at.elapsed(),
|
|
66
|
+
gc: self.gc,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Attach the request's timing to a response head when the access log
|
|
72
|
+
/// wants it; the extension is the one allocation an idle log skips.
|
|
73
|
+
fn timed(
|
|
74
|
+
ctx: &RequestCtx,
|
|
75
|
+
builder: hyper::http::response::Builder,
|
|
76
|
+
) -> hyper::http::response::Builder {
|
|
77
|
+
if ctx.timed {
|
|
78
|
+
builder.extension(ctx.timing())
|
|
79
|
+
} else {
|
|
80
|
+
builder
|
|
81
|
+
}
|
|
44
82
|
}
|
|
45
83
|
|
|
46
84
|
impl Drop for RequestCtx {
|
|
@@ -230,7 +268,7 @@ pub fn respond_simple(
|
|
|
230
268
|
body: RString,
|
|
231
269
|
) -> Result<bool, Error> {
|
|
232
270
|
let ctx = request.0.borrow();
|
|
233
|
-
let builder = build_head(status, headers)
|
|
271
|
+
let builder = timed(&ctx, build_head(status, headers)?);
|
|
234
272
|
let bytes = body_bytes(&ctx, body);
|
|
235
273
|
let response = builder
|
|
236
274
|
.body(full_body(bytes))
|
|
@@ -249,6 +287,13 @@ fn body_bytes(ctx: &RequestCtx, body: RString) -> Bytes {
|
|
|
249
287
|
}
|
|
250
288
|
|
|
251
289
|
impl Request {
|
|
290
|
+
/// The worker's measurements around the app call, for the access log's
|
|
291
|
+
/// breakdown: the GC pause in nanoseconds and the objects allocated.
|
|
292
|
+
/// Called only when the access log is on.
|
|
293
|
+
pub fn set_timing(_ruby: &Ruby, rb_self: &Request, gc_nanos: u64, allocs: u64) {
|
|
294
|
+
rb_self.0.borrow_mut().gc = Some((std::time::Duration::from_nanos(gc_nanos), allocs));
|
|
295
|
+
}
|
|
296
|
+
|
|
252
297
|
/// Next chunk of the request body, at most `max_len` bytes; nil at EOF.
|
|
253
298
|
/// Blocks (GVL released) until the client sends more.
|
|
254
299
|
pub fn read_body(
|
|
@@ -303,7 +348,7 @@ impl Request {
|
|
|
303
348
|
headers: RHash,
|
|
304
349
|
) -> Result<bool, Error> {
|
|
305
350
|
let ctx = rb_self.0.borrow();
|
|
306
|
-
let builder = build_head(status, headers)
|
|
351
|
+
let builder = timed(&ctx, build_head(status, headers)?);
|
|
307
352
|
ctx.responder
|
|
308
353
|
.send_stream_head(builder)
|
|
309
354
|
.map_err(|e| invalid_response(ruby, e))
|
|
@@ -425,6 +470,11 @@ pub fn test_ctx() -> crate::registry::BoxedCtx {
|
|
|
425
470
|
slot: None,
|
|
426
471
|
pin_slab: Arc::new(crate::pin::PinSlab::new()),
|
|
427
472
|
responder: Arc::new(Responder::new(head_tx)),
|
|
473
|
+
enqueued_at: std::time::Instant::now(),
|
|
474
|
+
timed: false,
|
|
475
|
+
wait: std::time::Duration::ZERO,
|
|
476
|
+
admitted_at: std::time::Instant::now(),
|
|
477
|
+
gc: None,
|
|
428
478
|
})
|
|
429
479
|
}
|
|
430
480
|
|
data/ext/kino/src/server.rs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//! intake. Ruby is never on these threads; the only contact points are the
|
|
3
3
|
//! flume queue (in) and each request's Responder (out).
|
|
4
4
|
|
|
5
|
-
use std::net::SocketAddr;
|
|
5
|
+
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
6
6
|
use std::sync::atomic::Ordering;
|
|
7
7
|
use std::sync::Arc;
|
|
8
8
|
use std::time::Duration;
|
|
@@ -13,6 +13,7 @@ use hyper_util::rt::TokioIo;
|
|
|
13
13
|
use magnus::{Error, Ruby};
|
|
14
14
|
use parking_lot::{Mutex, RwLock};
|
|
15
15
|
|
|
16
|
+
use crate::listen::Listener;
|
|
16
17
|
use crate::registry::{self, BoxedCtx, ServerInner, WorkerSlot};
|
|
17
18
|
use crate::request::RequestCtx;
|
|
18
19
|
use crate::response::{plain_response, HyperResponse, Responder};
|
|
@@ -44,8 +45,10 @@ fn cfg_opt<T: magnus::TryConvert>(
|
|
|
44
45
|
/// at boot, so Hash-lookup cost is irrelevant and the interface stays
|
|
45
46
|
/// extensible. Binding is synchronous so address errors raise in Ruby at
|
|
46
47
|
/// `start` time; returns the actual port for `port: 0`. TLS config errors
|
|
47
|
-
/// (bad cert/key) also raise here, before any traffic.
|
|
48
|
-
|
|
48
|
+
/// (bad cert/key) also raise here, before any traffic. The third element
|
|
49
|
+
/// of the return tuple is the control-plane port (nil unless control_bind
|
|
50
|
+
/// is configured).
|
|
51
|
+
pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Option<u16>), Error> {
|
|
49
52
|
let bind: String = cfg(ruby, config, "bind")?;
|
|
50
53
|
let port: u16 = cfg(ruby, config, "port")?;
|
|
51
54
|
let queue_depth: usize = cfg(ruby, config, "queue_depth")?;
|
|
@@ -58,6 +61,10 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16), Er
|
|
|
58
61
|
let tls_key: Option<String> = cfg_opt(ruby, config, "tls_key")?;
|
|
59
62
|
let lanes: bool = cfg_opt(ruby, config, "lanes")?.unwrap_or(false);
|
|
60
63
|
let log_requests: bool = cfg_opt(ruby, config, "log_requests")?.unwrap_or(false);
|
|
64
|
+
let mode: String = cfg_opt(ruby, config, "mode")?.unwrap_or_else(|| "threaded".to_string());
|
|
65
|
+
let workers: usize = cfg_opt(ruby, config, "workers")?.unwrap_or(0);
|
|
66
|
+
let threads: usize = cfg_opt(ruby, config, "threads")?.unwrap_or(0);
|
|
67
|
+
let batch: usize = cfg_opt(ruby, config, "batch")?.unwrap_or(1);
|
|
61
68
|
let acceptor = match (&tls_cert, &tls_key) {
|
|
62
69
|
(Some(cert), Some(key)) => Some(
|
|
63
70
|
crate::tls::build_acceptor(cert, key)
|
|
@@ -72,15 +79,32 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16), Er
|
|
|
72
79
|
}
|
|
73
80
|
};
|
|
74
81
|
|
|
75
|
-
let listener =
|
|
76
|
-
.map_err(|e| io_error(ruby, "bind failed", e))?;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
let listener =
|
|
83
|
+
Listener::bind(&bind, port).map_err(|e| io_error(ruby, "bind failed", e))?;
|
|
84
|
+
// Ruby refuses this combination up front; this guards embedders
|
|
85
|
+
// calling the native layer directly.
|
|
86
|
+
if acceptor.is_some() && matches!(listener, Listener::Unix(..)) {
|
|
87
|
+
return Err(Error::new(
|
|
88
|
+
ruby.exception_arg_error(),
|
|
89
|
+
"TLS is not supported on a unix socket bind",
|
|
90
|
+
));
|
|
91
|
+
}
|
|
80
92
|
let local_port = listener
|
|
81
|
-
.
|
|
82
|
-
.map_err(|e| io_error(ruby, "listener setup failed", e))
|
|
83
|
-
|
|
93
|
+
.port()
|
|
94
|
+
.map_err(|e| io_error(ruby, "listener setup failed", e))?;
|
|
95
|
+
let unix_path = match &listener {
|
|
96
|
+
Listener::Unix(_, path) => Some(path.clone()),
|
|
97
|
+
Listener::Tcp(_) => None,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let control_bind_addr: Option<String> = cfg_opt(ruby, config, "control_bind")?;
|
|
101
|
+
let control_token: Option<String> = cfg_opt(ruby, config, "control_token")?;
|
|
102
|
+
let control_bind = control_bind_addr
|
|
103
|
+
.as_deref()
|
|
104
|
+
.map(|addr| {
|
|
105
|
+
crate::control::bind_control(addr).map_err(|e| io_error(ruby, "control bind failed", e))
|
|
106
|
+
})
|
|
107
|
+
.transpose()?;
|
|
84
108
|
|
|
85
109
|
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
|
86
110
|
builder.enable_all().thread_name("kino-tokio");
|
|
@@ -108,17 +132,22 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16), Er
|
|
|
108
132
|
request_timeout_ms,
|
|
109
133
|
max_body_size,
|
|
110
134
|
timeouts: std::sync::atomic::AtomicU64::new(0),
|
|
135
|
+
state: std::sync::atomic::AtomicU8::new(registry::STATE_BOOTING),
|
|
136
|
+
respawns: std::sync::atomic::AtomicU64::new(0),
|
|
137
|
+
quarantine_replacements: std::sync::atomic::AtomicU64::new(0),
|
|
138
|
+
topology: registry::Topology { mode, workers, threads, batch },
|
|
111
139
|
https: acceptor.is_some(),
|
|
140
|
+
unix_path,
|
|
112
141
|
access_log: log_requests.then(|| crate::logsink::Sink::new(std::io::stdout())),
|
|
113
142
|
lanes,
|
|
114
143
|
lane_cursor: std::sync::atomic::AtomicUsize::new(0),
|
|
115
144
|
pin_slab: Arc::new(crate::pin::PinSlab::new()),
|
|
145
|
+
queue_histogram: registry::QueueHistogram::new(),
|
|
116
146
|
});
|
|
117
147
|
|
|
118
148
|
let tokio_listener = {
|
|
119
149
|
let _guard = runtime.enter();
|
|
120
|
-
|
|
121
|
-
.map_err(|e| io_error(ruby, "listener setup failed", e))?
|
|
150
|
+
AsyncListener::from_std(listener).map_err(|e| io_error(ruby, "listener setup failed", e))?
|
|
122
151
|
};
|
|
123
152
|
runtime.spawn(accept_loop(
|
|
124
153
|
tokio_listener,
|
|
@@ -130,8 +159,28 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16), Er
|
|
|
130
159
|
*server.runtime.lock() = Some(runtime);
|
|
131
160
|
|
|
132
161
|
let id = server.id;
|
|
162
|
+
let control_port = match control_bind {
|
|
163
|
+
// Not yet in the registry: on failure Ruby never learns this id, so
|
|
164
|
+
// nothing could ever reach it to shut it down. The accept loop's
|
|
165
|
+
// task holds its own Arc back to `server` (stored inside its own
|
|
166
|
+
// `runtime` field), so just dropping our handle would leak the
|
|
167
|
+
// runtime forever; take it out and stop it explicitly instead.
|
|
168
|
+
// Safe to block here: this is the plain Ruby thread, no async
|
|
169
|
+
// context above it.
|
|
170
|
+
Some(bind) => match crate::control::start(bind, server.clone(), control_token) {
|
|
171
|
+
Ok(port) => port,
|
|
172
|
+
Err(e) => {
|
|
173
|
+
// A plain drop blocks until the accept loop's task (its
|
|
174
|
+
// only task, idling on accept/shutdown) is torn down; the
|
|
175
|
+
// runtime only ever had this one thing to cancel.
|
|
176
|
+
drop(server.runtime.lock().take());
|
|
177
|
+
return Err(io_error(ruby, "control start failed", e));
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
None => None,
|
|
181
|
+
};
|
|
133
182
|
registry::insert(server);
|
|
134
|
-
Ok((id, local_port))
|
|
183
|
+
Ok((id, local_port, control_port))
|
|
135
184
|
}
|
|
136
185
|
|
|
137
186
|
/// Slowloris guard for TLS: a client that completes the TCP connect but then
|
|
@@ -142,8 +191,59 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16), Er
|
|
|
142
191
|
/// timeout: not a knob.
|
|
143
192
|
const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
|
|
144
193
|
|
|
194
|
+
/// What a unix-socket connection reports as its addresses. The peer is
|
|
195
|
+
/// local by definition (REMOTE_ADDR 127.0.0.1), and a socket has no port,
|
|
196
|
+
/// so SERVER_PORT falls back to http's default when the Host header names
|
|
197
|
+
/// none, the way Puma reports unix-socket requests.
|
|
198
|
+
const UNIX_PEER: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
|
|
199
|
+
const UNIX_LOCAL: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 80);
|
|
200
|
+
|
|
201
|
+
/// The accept loop's listener: TCP (optionally behind TLS), or a unix
|
|
202
|
+
/// socket, which carries plain HTTP only.
|
|
203
|
+
enum AsyncListener {
|
|
204
|
+
Tcp(tokio::net::TcpListener),
|
|
205
|
+
Unix(tokio::net::UnixListener),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// One accepted connection, before the protocol layer sees it.
|
|
209
|
+
enum Conn {
|
|
210
|
+
Tcp(tokio::net::TcpStream),
|
|
211
|
+
Unix(tokio::net::UnixStream),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
impl AsyncListener {
|
|
215
|
+
/// Register the bound listener with the current runtime.
|
|
216
|
+
fn from_std(listener: Listener) -> std::io::Result<AsyncListener> {
|
|
217
|
+
Ok(match listener {
|
|
218
|
+
Listener::Tcp(listener) => AsyncListener::Tcp(tokio::net::TcpListener::from_std(listener)?),
|
|
219
|
+
Listener::Unix(listener, _) => {
|
|
220
|
+
AsyncListener::Unix(tokio::net::UnixListener::from_std(listener)?)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// The next connection with its (peer, local) addresses.
|
|
226
|
+
async fn accept(&self) -> std::io::Result<(Conn, SocketAddr, SocketAddr)> {
|
|
227
|
+
match self {
|
|
228
|
+
AsyncListener::Tcp(listener) => {
|
|
229
|
+
let (stream, remote_addr) = listener.accept().await?;
|
|
230
|
+
// Small responses must not wait on Nagle + delayed ACK.
|
|
231
|
+
let _ = stream.set_nodelay(true);
|
|
232
|
+
let local_addr = stream
|
|
233
|
+
.local_addr()
|
|
234
|
+
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
|
|
235
|
+
Ok((Conn::Tcp(stream), remote_addr, local_addr))
|
|
236
|
+
}
|
|
237
|
+
AsyncListener::Unix(listener) => {
|
|
238
|
+
let (stream, _) = listener.accept().await?;
|
|
239
|
+
Ok((Conn::Unix(stream), UNIX_PEER, UNIX_LOCAL))
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
145
245
|
async fn accept_loop(
|
|
146
|
-
listener:
|
|
246
|
+
listener: AsyncListener,
|
|
147
247
|
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
148
248
|
server: Arc<ServerInner>,
|
|
149
249
|
max_connections: usize,
|
|
@@ -162,25 +262,20 @@ async fn accept_loop(
|
|
|
162
262
|
Err(_) => break, // semaphore closed
|
|
163
263
|
},
|
|
164
264
|
};
|
|
165
|
-
let (
|
|
265
|
+
let (conn, remote_addr, local_addr) = tokio::select! {
|
|
166
266
|
_ = shutdown_rx.changed() => break,
|
|
167
267
|
accepted = listener.accept() => match accepted {
|
|
168
|
-
Ok(
|
|
268
|
+
Ok(accepted) => accepted,
|
|
169
269
|
Err(_) => continue, // transient accept error; permit drops, retry
|
|
170
270
|
},
|
|
171
271
|
};
|
|
172
|
-
// Small responses must not wait on Nagle + delayed ACK.
|
|
173
|
-
let _ = stream.set_nodelay(true);
|
|
174
|
-
let local_addr = stream
|
|
175
|
-
.local_addr()
|
|
176
|
-
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
|
|
177
272
|
let server = server.clone();
|
|
178
273
|
let acceptor = acceptor.clone();
|
|
179
274
|
tokio::spawn(async move {
|
|
180
275
|
// Held for the connection's lifetime; dropping it frees a slot.
|
|
181
276
|
let _permit = permit;
|
|
182
|
-
match acceptor {
|
|
183
|
-
Some(acceptor) => {
|
|
277
|
+
match (conn, acceptor) {
|
|
278
|
+
(Conn::Tcp(stream), Some(acceptor)) => {
|
|
184
279
|
// Handshake failures (port scans, plain HTTP to a TLS
|
|
185
280
|
// port) and stalled handshakes (slowloris) just drop the
|
|
186
281
|
// connection; the timeout bounds the latter.
|
|
@@ -188,7 +283,13 @@ async fn accept_loop(
|
|
|
188
283
|
let Ok(Ok(tls)) = handshake.await else { return };
|
|
189
284
|
serve_connection(tls, server, remote_addr, local_addr).await;
|
|
190
285
|
}
|
|
191
|
-
|
|
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
|
+
}
|
|
192
293
|
}
|
|
193
294
|
});
|
|
194
295
|
}
|
|
@@ -270,18 +371,17 @@ async fn handle_request(
|
|
|
270
371
|
let (parts, body) = req.into_parts();
|
|
271
372
|
|
|
272
373
|
// Access-log metadata is captured only when logging is on: one Instant
|
|
273
|
-
// read plus
|
|
274
|
-
|
|
374
|
+
// read plus two small Strings per request. The arrival record is
|
|
375
|
+
// queued now, before the app sees the request, so a hang shows as an
|
|
376
|
+
// arrow with no answer; the completion record follows the response.
|
|
377
|
+
let log_meta = server.access_log.as_ref().map(|log| {
|
|
275
378
|
let target = match parts.uri.query() {
|
|
276
379
|
Some(q) => format!("{}?{}", parts.uri.path(), q),
|
|
277
380
|
None => parts.uri.path().to_string(),
|
|
278
381
|
};
|
|
279
|
-
(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
target,
|
|
283
|
-
parts.version,
|
|
284
|
-
)
|
|
382
|
+
let method = parts.method.to_string();
|
|
383
|
+
log.write_line(crate::access_log::arrival(&method, &target, remote_addr.ip()));
|
|
384
|
+
(std::time::Instant::now(), method, target)
|
|
285
385
|
});
|
|
286
386
|
|
|
287
387
|
// Body-size guard: an honestly-declared oversize body is refused with a
|
|
@@ -338,6 +438,7 @@ async fn handle_request(
|
|
|
338
438
|
|
|
339
439
|
let (head_tx, head_rx) = tokio::sync::oneshot::channel();
|
|
340
440
|
let responder = Arc::new(Responder::new(head_tx));
|
|
441
|
+
let now = std::time::Instant::now();
|
|
341
442
|
let ctx = Box::new(RequestCtx {
|
|
342
443
|
method: parts.method,
|
|
343
444
|
uri: parts.uri,
|
|
@@ -353,6 +454,11 @@ async fn handle_request(
|
|
|
353
454
|
slot: None,
|
|
354
455
|
pin_slab: server.pin_slab.clone(),
|
|
355
456
|
responder,
|
|
457
|
+
enqueued_at: now,
|
|
458
|
+
timed: server.access_log.is_some(),
|
|
459
|
+
wait: Duration::ZERO,
|
|
460
|
+
admitted_at: now,
|
|
461
|
+
gc: None,
|
|
356
462
|
});
|
|
357
463
|
|
|
358
464
|
// Drop guard, not manual decrement: when a client aborts mid-request,
|
|
@@ -431,17 +537,20 @@ async fn handle_request(
|
|
|
431
537
|
}
|
|
432
538
|
};
|
|
433
539
|
|
|
434
|
-
if let (Some(log), Some((start, method, target
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
let
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
540
|
+
if let (Some(log), Some((start, method, target))) = (server.access_log.as_ref(), log_meta) {
|
|
541
|
+
// The worker attached its timing to the response head; a 503 or
|
|
542
|
+
// 504 never reached a worker and carries none.
|
|
543
|
+
let timing = response
|
|
544
|
+
.extensions()
|
|
545
|
+
.get::<crate::access_log::Timing>()
|
|
546
|
+
.copied();
|
|
547
|
+
log.write_line(crate::access_log::completion(
|
|
548
|
+
response.status().as_u16(),
|
|
549
|
+
&method,
|
|
550
|
+
&target,
|
|
551
|
+
start.elapsed(),
|
|
552
|
+
timing,
|
|
553
|
+
));
|
|
445
554
|
}
|
|
446
555
|
|
|
447
556
|
Ok(branded(response))
|
|
@@ -518,11 +627,28 @@ pub fn register_worker(ruby: &Ruby, server_id: u64) -> Result<usize, Error> {
|
|
|
518
627
|
|
|
519
628
|
pub fn stop_accepting(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
520
629
|
if let Some(server) = registry::try_get(server_id) {
|
|
630
|
+
server.state.store(registry::STATE_DRAINING, Ordering::Relaxed);
|
|
521
631
|
let _ = server.shutdown_tx.send(true);
|
|
522
632
|
}
|
|
523
633
|
Ok(())
|
|
524
634
|
}
|
|
525
635
|
|
|
636
|
+
/// Ruby reports the worker pool up; /ready starts answering 200.
|
|
637
|
+
pub fn control_ready(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
638
|
+
if let Some(server) = registry::try_get(server_id) {
|
|
639
|
+
server.state.store(registry::STATE_READY, Ordering::Relaxed);
|
|
640
|
+
}
|
|
641
|
+
Ok(())
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/// One worker respawn, recorded by the Ruby supervisor.
|
|
645
|
+
pub fn record_respawn(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
646
|
+
if let Some(server) = registry::try_get(server_id) {
|
|
647
|
+
server.respawns.fetch_add(1, Ordering::Relaxed);
|
|
648
|
+
}
|
|
649
|
+
Ok(())
|
|
650
|
+
}
|
|
651
|
+
|
|
526
652
|
pub fn close_queue(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
527
653
|
if let Some(server) = registry::try_get(server_id) {
|
|
528
654
|
server.req_tx.lock().take();
|
|
@@ -546,6 +672,12 @@ fn abort_slot(slot: &WorkerSlot) {
|
|
|
546
672
|
responder.respond_500_if_unsent();
|
|
547
673
|
}
|
|
548
674
|
}
|
|
675
|
+
// A dead worker holds nothing: every request it had is answered above
|
|
676
|
+
// (or already was), so the slot is quiescent from here on. Without
|
|
677
|
+
// this, a crashed worker's slot reports in_flight>=1 forever (the
|
|
678
|
+
// supervisor never reuses a slot after a crash), wedging /stats,
|
|
679
|
+
// /metrics and server.stats with a phantom busy worker.
|
|
680
|
+
slot.in_flight.store(0, Ordering::Relaxed);
|
|
549
681
|
// Lane mode: this worker is dead. Close its lane so the dispatcher
|
|
550
682
|
// skips it, and drain anything queued; dropping each ctx fires the
|
|
551
683
|
// Drop-500 backstop so those clients aren't left hanging.
|
|
@@ -587,6 +719,10 @@ pub fn shutdown_runtime(_ruby: &Ruby, server_id: u64, timeout_ms: u64) -> Result
|
|
|
587
719
|
if let Some(runtime) = server.runtime.lock().take() {
|
|
588
720
|
runtime.shutdown_timeout(Duration::from_millis(timeout_ms));
|
|
589
721
|
}
|
|
722
|
+
// The listener is closed with the runtime; its socket file is not.
|
|
723
|
+
if let Some(path) = &server.unix_path {
|
|
724
|
+
crate::listen::cleanup_unix(path);
|
|
725
|
+
}
|
|
590
726
|
}
|
|
591
727
|
Ok(())
|
|
592
728
|
}
|
|
@@ -601,21 +737,15 @@ pub fn pin_keeper(
|
|
|
601
737
|
Ok(ruby.obj_wrap(crate::pin::PinKeeper(server.pin_slab.clone())))
|
|
602
738
|
}
|
|
603
739
|
|
|
604
|
-
/// Errors print in red on color terminals. Covers worker errors,
|
|
605
|
-
/// supervisor crash reports, and everything apps write to rack.errors.
|
|
606
|
-
pub fn log_error(message: String) {
|
|
607
|
-
eprintln!("{}", crate::style::red(&format!("[Kino] {message}")));
|
|
608
|
-
}
|
|
609
|
-
|
|
610
740
|
/// Full stats snapshot: [queued, in_flight, served, rejected, timeouts,
|
|
611
|
-
/// lane_depths]. lane_depths is nil unless lane dispatch is on.
|
|
741
|
+
/// respawns, lane_depths]. lane_depths is nil unless lane dispatch is on.
|
|
612
742
|
#[allow(clippy::type_complexity)]
|
|
613
743
|
pub fn server_stats(
|
|
614
744
|
_ruby: &Ruby,
|
|
615
745
|
server_id: u64,
|
|
616
|
-
) -> Result<(usize, usize, u64, u64, u64, Option<Vec<usize>>), Error> {
|
|
746
|
+
) -> Result<(usize, usize, u64, u64, u64, u64, Option<Vec<usize>>), Error> {
|
|
617
747
|
let Some(server) = registry::try_get(server_id) else {
|
|
618
|
-
return Ok((0, 0, 0, 0, 0, None));
|
|
748
|
+
return Ok((0, 0, 0, 0, 0, 0, None));
|
|
619
749
|
};
|
|
620
750
|
let lane_depths = server.lane_depths();
|
|
621
751
|
let queued = server.req_rx.len() + lane_depths.as_ref().map_or(0, |d| d.iter().sum::<usize>());
|
|
@@ -625,10 +755,56 @@ pub fn server_stats(
|
|
|
625
755
|
server.served.load(Ordering::Relaxed),
|
|
626
756
|
server.rejected.load(Ordering::Relaxed),
|
|
627
757
|
server.timeouts.load(Ordering::Relaxed),
|
|
758
|
+
server.respawns.load(Ordering::Relaxed),
|
|
628
759
|
lane_depths,
|
|
629
760
|
))
|
|
630
761
|
}
|
|
631
762
|
|
|
763
|
+
/// Queue-wait count and summed seconds for Server#stats parity. Zeros when
|
|
764
|
+
/// the server is gone.
|
|
765
|
+
pub fn queue_time(_ruby: &Ruby, server_id: u64) -> Result<(u64, f64), Error> {
|
|
766
|
+
let Some(server) = registry::try_get(server_id) else {
|
|
767
|
+
return Ok((0, 0.0));
|
|
768
|
+
};
|
|
769
|
+
let h = server.queue_histogram.snapshot();
|
|
770
|
+
Ok((h.count, h.sum_seconds()))
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/// One worker slot's [index, served, in_flight, busy_ms, quarantined] row.
|
|
774
|
+
pub type WorkerStatRow = (usize, u64, usize, u64, bool);
|
|
775
|
+
|
|
776
|
+
/// Per-slot rows for Server#stats parity: [index, served, in_flight,
|
|
777
|
+
/// 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> {
|
|
782
|
+
let Some(server) = registry::try_get(server_id) else {
|
|
783
|
+
return Ok(Vec::new());
|
|
784
|
+
};
|
|
785
|
+
Ok(crate::control::collect_worker_status(&server)
|
|
786
|
+
.into_iter()
|
|
787
|
+
.map(|w| (w.index, w.served, w.in_flight, w.busy_ms, w.quarantined))
|
|
788
|
+
.collect())
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/// Mark a slot quarantined (the monitor has abandoned it as wedged).
|
|
792
|
+
pub fn quarantine_slot(ruby: &Ruby, server_id: u64, worker_id: usize) -> Result<(), Error> {
|
|
793
|
+
if let Some(server) = registry::try_get(server_id) {
|
|
794
|
+
let slot = server.slot(ruby, worker_id)?;
|
|
795
|
+
slot.quarantined.store(true, Ordering::Relaxed);
|
|
796
|
+
}
|
|
797
|
+
Ok(())
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/// One replacement spawned by the quarantine monitor.
|
|
801
|
+
pub fn record_quarantine_replacement(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
802
|
+
if let Some(server) = registry::try_get(server_id) {
|
|
803
|
+
server.quarantine_replacements.fetch_add(1, Ordering::Relaxed);
|
|
804
|
+
}
|
|
805
|
+
Ok(())
|
|
806
|
+
}
|
|
807
|
+
|
|
632
808
|
#[cfg(test)]
|
|
633
809
|
mod tests {
|
|
634
810
|
use super::*;
|
data/ext/kino/src/style.rs
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
//! ANSI styling for
|
|
2
|
-
//!
|
|
3
|
-
//! startup banner has its own twin in Kino::CLI.) Color
|
|
4
|
-
//! decided once per stream; every styled string resets at
|
|
5
|
-
//! nothing bleeds.
|
|
1
|
+
//! ANSI styling for what the native layer writes to the terminal: the
|
|
2
|
+
//! access log on stdout and server log lines on either stream. (The
|
|
3
|
+
//! Ruby-side startup banner has its own twin in Kino::CLI.) Color
|
|
4
|
+
//! capability is decided once per stream; every styled string resets at
|
|
5
|
+
//! its end so nothing bleeds.
|
|
6
6
|
|
|
7
7
|
use std::sync::OnceLock;
|
|
8
8
|
|
|
9
9
|
#[derive(Clone, Copy)]
|
|
10
|
-
enum Stream {
|
|
10
|
+
pub enum Stream {
|
|
11
11
|
Stdout,
|
|
12
12
|
Stderr,
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/// Whether `stream` is a color terminal: a tty, no NO_COLOR, TERM not dumb.
|
|
16
|
+
pub fn enabled(stream: Stream) -> bool {
|
|
16
17
|
use std::io::IsTerminal;
|
|
17
18
|
static STDOUT: OnceLock<bool> = OnceLock::new();
|
|
18
19
|
static STDERR: OnceLock<bool> = OnceLock::new();
|
|
@@ -27,55 +28,51 @@ fn enabled(stream: Stream) -> bool {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/// Wrap `text` in an SGR code (
|
|
31
|
-
///
|
|
32
|
-
fn
|
|
33
|
-
if
|
|
31
|
+
/// Wrap `text` in an SGR code ("1" bold, "91" bright red, "1;32" bold
|
|
32
|
+
/// green) when `color`, resetting at the end; plain otherwise.
|
|
33
|
+
pub fn sgr(code: &str, text: &str, color: bool) -> String {
|
|
34
|
+
if color {
|
|
34
35
|
format!("\x1b[{code}m{text}\x1b[0m")
|
|
35
36
|
} else {
|
|
36
37
|
text.to_string()
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
///
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
/// Dark gray: timestamps and the timing breakdown recede behind the record.
|
|
42
|
+
pub const DIM: &str = "90";
|
|
43
|
+
/// Bold bright white: the arrival line, which has no status to color by.
|
|
44
|
+
pub const BOLD_WHITE: &str = "1;97";
|
|
45
|
+
/// Warnings are yellow.
|
|
46
|
+
pub const WARN: &str = "33";
|
|
47
|
+
/// Errors are bright red (91): the base red slot (31) is remapped to odd
|
|
48
|
+
/// hues by some terminal themes; 91 stays red.
|
|
49
|
+
pub const ERROR: &str = "91";
|
|
45
50
|
|
|
46
|
-
/// The SGR code for a status class
|
|
47
|
-
/// 2xx green, 3xx yellow, 4xx maroon
|
|
48
|
-
/// 5xx bright red, anything else uncolored.
|
|
49
|
-
fn status_sgr(status: u16) -> Option<&'static str> {
|
|
51
|
+
/// The SGR code for a status class, bold so the record leads its line
|
|
52
|
+
/// (basic 16-color palette only): 2xx green, 3xx yellow, 4xx maroon
|
|
53
|
+
/// (ANSI color 1, plain dark red), 5xx bright red, anything else uncolored.
|
|
54
|
+
pub fn status_sgr(status: u16) -> Option<&'static str> {
|
|
50
55
|
match status {
|
|
51
|
-
200..=299 => Some("32"),
|
|
52
|
-
300..=399 => Some("33"),
|
|
53
|
-
400..=499 => Some("31"),
|
|
54
|
-
500..=599 => Some("91"),
|
|
56
|
+
200..=299 => Some("1;32"),
|
|
57
|
+
300..=399 => Some("1;33"),
|
|
58
|
+
400..=499 => Some("1;31"),
|
|
59
|
+
500..=599 => Some("1;91"),
|
|
55
60
|
_ => None,
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
/// Access-log lines on stdout, tinted by status class.
|
|
60
|
-
pub fn status_colored(status: u16, line: &str) -> String {
|
|
61
|
-
match status_sgr(status) {
|
|
62
|
-
Some(code) => paint(Stream::Stdout, code, line),
|
|
63
|
-
None => line.to_string(),
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
64
|
#[cfg(test)]
|
|
68
65
|
mod tests {
|
|
69
|
-
use super::status_sgr;
|
|
66
|
+
use super::{sgr, status_sgr};
|
|
70
67
|
|
|
71
68
|
#[test]
|
|
72
69
|
fn status_classes_map_to_their_colors() {
|
|
73
|
-
assert_eq!(status_sgr(200), Some("32")); // green
|
|
74
|
-
assert_eq!(status_sgr(299), Some("32"));
|
|
75
|
-
assert_eq!(status_sgr(301), Some("33")); // yellow
|
|
76
|
-
assert_eq!(status_sgr(404), Some("31")); // maroon
|
|
77
|
-
assert_eq!(status_sgr(500), Some("91")); // bright red
|
|
78
|
-
assert_eq!(status_sgr(599), Some("91"));
|
|
70
|
+
assert_eq!(status_sgr(200), Some("1;32")); // green
|
|
71
|
+
assert_eq!(status_sgr(299), Some("1;32"));
|
|
72
|
+
assert_eq!(status_sgr(301), Some("1;33")); // yellow
|
|
73
|
+
assert_eq!(status_sgr(404), Some("1;31")); // maroon
|
|
74
|
+
assert_eq!(status_sgr(500), Some("1;91")); // bright red
|
|
75
|
+
assert_eq!(status_sgr(599), Some("1;91"));
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
#[test]
|
|
@@ -84,4 +81,10 @@ mod tests {
|
|
|
84
81
|
assert_eq!(status_sgr(199), None);
|
|
85
82
|
assert_eq!(status_sgr(600), None);
|
|
86
83
|
}
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn sgr_wraps_and_resets_only_when_coloring() {
|
|
87
|
+
assert_eq!(sgr("1;32", "ok", true), "\x1b[1;32mok\x1b[0m");
|
|
88
|
+
assert_eq!(sgr("1;32", "ok", false), "ok");
|
|
89
|
+
}
|
|
87
90
|
}
|