kino 0.3.0 → 0.5.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 +55 -0
- data/Cargo.lock +154 -25
- data/README.md +71 -14
- data/doc/architecture.md +7 -4
- 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 +279 -89
- data/ext/kino/src/cpus.rs +37 -0
- data/ext/kino/src/env_strings.rs +10 -2
- data/ext/kino/src/io_shards.rs +437 -0
- data/ext/kino/src/lib.rs +11 -1
- data/ext/kino/src/listen.rs +137 -0
- data/ext/kino/src/log.rs +152 -0
- data/ext/kino/src/queue.rs +9 -4
- data/ext/kino/src/registry.rs +102 -10
- data/ext/kino/src/request.rs +48 -2
- data/ext/kino/src/response.rs +1 -4
- data/ext/kino/src/server.rs +207 -95
- data/ext/kino/src/style.rs +42 -39
- data/ext/kino/src/tls.rs +11 -4
- data/lib/kino/cli.rb +20 -9
- data/lib/kino/configuration.rb +30 -5
- data/lib/kino/errors_stream.rb +4 -3
- data/lib/kino/hook_fire.rb +3 -4
- data/lib/kino/log.rb +104 -0
- data/lib/kino/quarantine_monitor.rb +7 -4
- data/lib/kino/ractor_supervisor.rb +8 -3
- data/lib/kino/server.rb +68 -15
- data/lib/kino/templates/kino.rb.tt +19 -7
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +12 -13
- data/lib/kino/worker_hooks.rb +3 -1
- data/lib/kino.rb +11 -0
- data/lib/rackup/handler/kino.rb +88 -0
- data/sig/kino.rbs +64 -1
- metadata +22 -1
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,7 +13,8 @@ use hyper_util::rt::TokioIo;
|
|
|
13
13
|
use magnus::{Error, Ruby};
|
|
14
14
|
use parking_lot::{Mutex, RwLock};
|
|
15
15
|
|
|
16
|
-
use crate::
|
|
16
|
+
use crate::listen::Listener;
|
|
17
|
+
use crate::registry::{self, BoxedCtx, RuntimeHandle, ServerInner, WorkerSlot};
|
|
17
18
|
use crate::request::RequestCtx;
|
|
18
19
|
use crate::response::{plain_response, HyperResponse, Responder};
|
|
19
20
|
|
|
@@ -55,6 +56,8 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
55
56
|
let request_timeout_ms: u64 = cfg_opt::<u64>(ruby, config, "request_timeout_ms")?.unwrap_or(0);
|
|
56
57
|
let max_body_size: usize = cfg_opt::<usize>(ruby, config, "max_body_size")?.unwrap_or(0);
|
|
57
58
|
let max_connections: usize = cfg_opt::<usize>(ruby, config, "max_connections")?.unwrap_or(1024);
|
|
59
|
+
let io_shards: bool = cfg_opt(ruby, config, "io_shards")?.unwrap_or(false);
|
|
60
|
+
let io_threads: usize = cfg_opt::<usize>(ruby, config, "io_threads")?.unwrap_or(0);
|
|
58
61
|
let tokio_threads: usize = cfg_opt::<usize>(ruby, config, "tokio_threads")?.unwrap_or(0);
|
|
59
62
|
let tls_cert: Option<String> = cfg_opt(ruby, config, "tls_cert")?;
|
|
60
63
|
let tls_key: Option<String> = cfg_opt(ruby, config, "tls_key")?;
|
|
@@ -78,15 +81,22 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
78
81
|
}
|
|
79
82
|
};
|
|
80
83
|
|
|
81
|
-
let listener =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
let listener = Listener::bind(&bind, port).map_err(|e| io_error(ruby, "bind failed", e))?;
|
|
85
|
+
// Ruby refuses this combination up front; this guards embedders
|
|
86
|
+
// calling the native layer directly.
|
|
87
|
+
if acceptor.is_some() && matches!(listener, Listener::Unix(..)) {
|
|
88
|
+
return Err(Error::new(
|
|
89
|
+
ruby.exception_arg_error(),
|
|
90
|
+
"TLS is not supported on a unix socket bind",
|
|
91
|
+
));
|
|
92
|
+
}
|
|
86
93
|
let local_port = listener
|
|
87
|
-
.
|
|
88
|
-
.map_err(|e| io_error(ruby, "listener setup failed", e))
|
|
89
|
-
|
|
94
|
+
.port()
|
|
95
|
+
.map_err(|e| io_error(ruby, "listener setup failed", e))?;
|
|
96
|
+
let unix_path = match &listener {
|
|
97
|
+
Listener::Unix(_, path) => Some(path.clone()),
|
|
98
|
+
Listener::Tcp(_) => None,
|
|
99
|
+
};
|
|
90
100
|
|
|
91
101
|
let control_bind_addr: Option<String> = cfg_opt(ruby, config, "control_bind")?;
|
|
92
102
|
let control_token: Option<String> = cfg_opt(ruby, config, "control_token")?;
|
|
@@ -97,15 +107,6 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
97
107
|
})
|
|
98
108
|
.transpose()?;
|
|
99
109
|
|
|
100
|
-
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
|
101
|
-
builder.enable_all().thread_name("kino-tokio");
|
|
102
|
-
if tokio_threads > 0 {
|
|
103
|
-
builder.worker_threads(tokio_threads);
|
|
104
|
-
}
|
|
105
|
-
let runtime = builder
|
|
106
|
-
.build()
|
|
107
|
-
.map_err(|e| io_error(ruby, "tokio runtime failed", e))?;
|
|
108
|
-
|
|
109
110
|
let (req_tx, req_rx) = flume::bounded(queue_depth);
|
|
110
111
|
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
|
|
111
112
|
|
|
@@ -114,7 +115,7 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
114
115
|
req_tx: Mutex::new(Some(req_tx)),
|
|
115
116
|
req_rx,
|
|
116
117
|
shutdown_tx,
|
|
117
|
-
runtime: Mutex::new(None),
|
|
118
|
+
runtime: Mutex::new(RuntimeHandle::None),
|
|
118
119
|
slots: RwLock::new(Vec::new()),
|
|
119
120
|
in_flight: std::sync::atomic::AtomicUsize::new(0),
|
|
120
121
|
served: std::sync::atomic::AtomicU64::new(0),
|
|
@@ -126,8 +127,14 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
126
127
|
state: std::sync::atomic::AtomicU8::new(registry::STATE_BOOTING),
|
|
127
128
|
respawns: std::sync::atomic::AtomicU64::new(0),
|
|
128
129
|
quarantine_replacements: std::sync::atomic::AtomicU64::new(0),
|
|
129
|
-
topology: registry::Topology {
|
|
130
|
+
topology: registry::Topology {
|
|
131
|
+
mode,
|
|
132
|
+
workers,
|
|
133
|
+
threads,
|
|
134
|
+
batch,
|
|
135
|
+
},
|
|
130
136
|
https: acceptor.is_some(),
|
|
137
|
+
unix_path,
|
|
131
138
|
access_log: log_requests.then(|| crate::logsink::Sink::new(std::io::stdout())),
|
|
132
139
|
lanes,
|
|
133
140
|
lane_cursor: std::sync::atomic::AtomicUsize::new(0),
|
|
@@ -135,19 +142,48 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
135
142
|
queue_histogram: registry::QueueHistogram::new(),
|
|
136
143
|
});
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
if io_shards {
|
|
146
|
+
// The shards keep serving accepted connections while the acceptor
|
|
147
|
+
// drains on `shutdown_rx`; this second signal stops them only at
|
|
148
|
+
// final teardown.
|
|
149
|
+
let (runtime_shutdown_tx, runtime_shutdown_rx) = tokio::sync::watch::channel(false);
|
|
150
|
+
let threads = crate::io_shards::spawn(
|
|
151
|
+
listener,
|
|
152
|
+
acceptor,
|
|
153
|
+
server.clone(),
|
|
154
|
+
max_connections,
|
|
155
|
+
shutdown_rx,
|
|
156
|
+
runtime_shutdown_rx,
|
|
157
|
+
crate::io_shards::thread_count(io_threads),
|
|
158
|
+
)
|
|
159
|
+
.map_err(|e| io_error(ruby, "sharded runtime failed", e))?;
|
|
160
|
+
*server.runtime.lock() = RuntimeHandle::Shards {
|
|
161
|
+
shutdown_tx: runtime_shutdown_tx,
|
|
162
|
+
threads,
|
|
163
|
+
};
|
|
164
|
+
} else {
|
|
165
|
+
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
|
166
|
+
builder.enable_all().thread_name("kino-tokio");
|
|
167
|
+
if tokio_threads > 0 {
|
|
168
|
+
builder.worker_threads(tokio_threads);
|
|
169
|
+
}
|
|
170
|
+
let runtime = builder
|
|
171
|
+
.build()
|
|
172
|
+
.map_err(|e| io_error(ruby, "tokio runtime failed", e))?;
|
|
173
|
+
let tokio_listener = {
|
|
174
|
+
let _guard = runtime.enter();
|
|
175
|
+
AsyncListener::from_std(listener)
|
|
176
|
+
.map_err(|e| io_error(ruby, "listener setup failed", e))?
|
|
177
|
+
};
|
|
178
|
+
runtime.spawn(accept_loop(
|
|
179
|
+
tokio_listener,
|
|
180
|
+
acceptor,
|
|
181
|
+
server.clone(),
|
|
182
|
+
max_connections,
|
|
183
|
+
shutdown_rx,
|
|
184
|
+
));
|
|
185
|
+
*server.runtime.lock() = RuntimeHandle::MultiThread(runtime);
|
|
186
|
+
}
|
|
151
187
|
|
|
152
188
|
let id = server.id;
|
|
153
189
|
let control_port = match control_bind {
|
|
@@ -161,10 +197,11 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
161
197
|
Some(bind) => match crate::control::start(bind, server.clone(), control_token) {
|
|
162
198
|
Ok(port) => port,
|
|
163
199
|
Err(e) => {
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
200
|
+
// Nothing is serving yet, so the bound only matters for a
|
|
201
|
+
// wedged shard thread; the default runtime just cancels
|
|
202
|
+
// its one idle accept task.
|
|
203
|
+
let _ = server.shutdown_tx.send(true);
|
|
204
|
+
std::mem::take(&mut *server.runtime.lock()).shutdown(Duration::from_millis(1_000));
|
|
168
205
|
return Err(io_error(ruby, "control start failed", e));
|
|
169
206
|
}
|
|
170
207
|
},
|
|
@@ -182,8 +219,61 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
182
219
|
/// timeout: not a knob.
|
|
183
220
|
const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
|
|
184
221
|
|
|
222
|
+
/// What a unix-socket connection reports as its addresses. The peer is
|
|
223
|
+
/// local by definition (REMOTE_ADDR 127.0.0.1), and a socket has no port,
|
|
224
|
+
/// so SERVER_PORT falls back to http's default when the Host header names
|
|
225
|
+
/// none, the way Puma reports unix-socket requests.
|
|
226
|
+
const UNIX_PEER: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
|
|
227
|
+
const UNIX_LOCAL: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 80);
|
|
228
|
+
|
|
229
|
+
/// The accept loop's listener: TCP (optionally behind TLS), or a unix
|
|
230
|
+
/// socket, which carries plain HTTP only.
|
|
231
|
+
pub(crate) enum AsyncListener {
|
|
232
|
+
Tcp(tokio::net::TcpListener),
|
|
233
|
+
Unix(tokio::net::UnixListener),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// One accepted connection, before the protocol layer sees it.
|
|
237
|
+
pub(crate) enum Conn {
|
|
238
|
+
Tcp(tokio::net::TcpStream),
|
|
239
|
+
Unix(tokio::net::UnixStream),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
impl AsyncListener {
|
|
243
|
+
/// Register the bound listener with the current runtime.
|
|
244
|
+
pub(crate) fn from_std(listener: Listener) -> std::io::Result<AsyncListener> {
|
|
245
|
+
Ok(match listener {
|
|
246
|
+
Listener::Tcp(listener) => {
|
|
247
|
+
AsyncListener::Tcp(tokio::net::TcpListener::from_std(listener)?)
|
|
248
|
+
}
|
|
249
|
+
Listener::Unix(listener, _) => {
|
|
250
|
+
AsyncListener::Unix(tokio::net::UnixListener::from_std(listener)?)
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// The next connection with its (peer, local) addresses.
|
|
256
|
+
pub(crate) async fn accept(&self) -> std::io::Result<(Conn, SocketAddr, SocketAddr)> {
|
|
257
|
+
match self {
|
|
258
|
+
AsyncListener::Tcp(listener) => {
|
|
259
|
+
let (stream, remote_addr) = listener.accept().await?;
|
|
260
|
+
// Small responses must not wait on Nagle + delayed ACK.
|
|
261
|
+
let _ = stream.set_nodelay(true);
|
|
262
|
+
let local_addr = stream
|
|
263
|
+
.local_addr()
|
|
264
|
+
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
|
|
265
|
+
Ok((Conn::Tcp(stream), remote_addr, local_addr))
|
|
266
|
+
}
|
|
267
|
+
AsyncListener::Unix(listener) => {
|
|
268
|
+
let (stream, _) = listener.accept().await?;
|
|
269
|
+
Ok((Conn::Unix(stream), UNIX_PEER, UNIX_LOCAL))
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
185
275
|
async fn accept_loop(
|
|
186
|
-
listener:
|
|
276
|
+
listener: AsyncListener,
|
|
187
277
|
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
188
278
|
server: Arc<ServerInner>,
|
|
189
279
|
max_connections: usize,
|
|
@@ -202,38 +292,50 @@ async fn accept_loop(
|
|
|
202
292
|
Err(_) => break, // semaphore closed
|
|
203
293
|
},
|
|
204
294
|
};
|
|
205
|
-
let (
|
|
295
|
+
let (conn, remote_addr, local_addr) = tokio::select! {
|
|
206
296
|
_ = shutdown_rx.changed() => break,
|
|
207
297
|
accepted = listener.accept() => match accepted {
|
|
208
|
-
Ok(
|
|
298
|
+
Ok(accepted) => accepted,
|
|
209
299
|
Err(_) => continue, // transient accept error; permit drops, retry
|
|
210
300
|
},
|
|
211
301
|
};
|
|
212
|
-
// Small responses must not wait on Nagle + delayed ACK.
|
|
213
|
-
let _ = stream.set_nodelay(true);
|
|
214
|
-
let local_addr = stream
|
|
215
|
-
.local_addr()
|
|
216
|
-
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
|
|
217
302
|
let server = server.clone();
|
|
218
303
|
let acceptor = acceptor.clone();
|
|
219
304
|
tokio::spawn(async move {
|
|
220
305
|
// Held for the connection's lifetime; dropping it frees a slot.
|
|
221
306
|
let _permit = permit;
|
|
222
|
-
|
|
223
|
-
Some(acceptor) => {
|
|
224
|
-
// Handshake failures (port scans, plain HTTP to a TLS
|
|
225
|
-
// port) and stalled handshakes (slowloris) just drop the
|
|
226
|
-
// connection; the timeout bounds the latter.
|
|
227
|
-
let handshake = tokio::time::timeout(TLS_HANDSHAKE_TIMEOUT, acceptor.accept(stream));
|
|
228
|
-
let Ok(Ok(tls)) = handshake.await else { return };
|
|
229
|
-
serve_connection(tls, server, remote_addr, local_addr).await;
|
|
230
|
-
}
|
|
231
|
-
None => serve_connection(stream, server, remote_addr, local_addr).await,
|
|
232
|
-
}
|
|
307
|
+
serve_conn(conn, acceptor, server, remote_addr, local_addr).await;
|
|
233
308
|
});
|
|
234
309
|
}
|
|
235
310
|
}
|
|
236
311
|
|
|
312
|
+
/// Everything between an accepted connection and hyper: the optional TLS
|
|
313
|
+
/// handshake, then the protocol layer. Shared by the default accept loop
|
|
314
|
+
/// and the sharded one, so connection policy exists exactly once.
|
|
315
|
+
pub(crate) async fn serve_conn(
|
|
316
|
+
conn: Conn,
|
|
317
|
+
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
318
|
+
server: Arc<ServerInner>,
|
|
319
|
+
remote_addr: SocketAddr,
|
|
320
|
+
local_addr: SocketAddr,
|
|
321
|
+
) {
|
|
322
|
+
match (conn, acceptor) {
|
|
323
|
+
(Conn::Tcp(stream), Some(acceptor)) => {
|
|
324
|
+
// Handshake failures (port scans, plain HTTP to a TLS
|
|
325
|
+
// port) and stalled handshakes (slowloris) just drop the
|
|
326
|
+
// connection; the timeout bounds the latter.
|
|
327
|
+
let handshake = tokio::time::timeout(TLS_HANDSHAKE_TIMEOUT, acceptor.accept(stream));
|
|
328
|
+
let Ok(Ok(tls)) = handshake.await else { return };
|
|
329
|
+
serve_connection(tls, server, remote_addr, local_addr).await;
|
|
330
|
+
}
|
|
331
|
+
(Conn::Tcp(stream), None) => {
|
|
332
|
+
serve_connection(stream, server, remote_addr, local_addr).await
|
|
333
|
+
}
|
|
334
|
+
// TLS over a unix socket is refused at bind time.
|
|
335
|
+
(Conn::Unix(stream), _) => serve_connection(stream, server, remote_addr, local_addr).await,
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
237
339
|
/// Slowloris guard: drop a connection that has not sent its complete request
|
|
238
340
|
/// headers within this window. Long enough never to trip a real client (even
|
|
239
341
|
/// on a slow mobile link), short enough to reap a stalled one. Deliberately a
|
|
@@ -310,18 +412,21 @@ async fn handle_request(
|
|
|
310
412
|
let (parts, body) = req.into_parts();
|
|
311
413
|
|
|
312
414
|
// Access-log metadata is captured only when logging is on: one Instant
|
|
313
|
-
// read plus
|
|
314
|
-
|
|
415
|
+
// read plus two small Strings per request. The arrival record is
|
|
416
|
+
// queued now, before the app sees the request, so a hang shows as an
|
|
417
|
+
// arrow with no answer; the completion record follows the response.
|
|
418
|
+
let log_meta = server.access_log.as_ref().map(|log| {
|
|
315
419
|
let target = match parts.uri.query() {
|
|
316
420
|
Some(q) => format!("{}?{}", parts.uri.path(), q),
|
|
317
421
|
None => parts.uri.path().to_string(),
|
|
318
422
|
};
|
|
319
|
-
(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
target,
|
|
323
|
-
|
|
324
|
-
)
|
|
423
|
+
let method = parts.method.to_string();
|
|
424
|
+
log.write_line(crate::access_log::arrival(
|
|
425
|
+
&method,
|
|
426
|
+
&target,
|
|
427
|
+
remote_addr.ip(),
|
|
428
|
+
));
|
|
429
|
+
(std::time::Instant::now(), method, target)
|
|
325
430
|
});
|
|
326
431
|
|
|
327
432
|
// Body-size guard: an honestly-declared oversize body is refused with a
|
|
@@ -378,6 +483,7 @@ async fn handle_request(
|
|
|
378
483
|
|
|
379
484
|
let (head_tx, head_rx) = tokio::sync::oneshot::channel();
|
|
380
485
|
let responder = Arc::new(Responder::new(head_tx));
|
|
486
|
+
let now = std::time::Instant::now();
|
|
381
487
|
let ctx = Box::new(RequestCtx {
|
|
382
488
|
method: parts.method,
|
|
383
489
|
uri: parts.uri,
|
|
@@ -393,7 +499,11 @@ async fn handle_request(
|
|
|
393
499
|
slot: None,
|
|
394
500
|
pin_slab: server.pin_slab.clone(),
|
|
395
501
|
responder,
|
|
396
|
-
enqueued_at:
|
|
502
|
+
enqueued_at: now,
|
|
503
|
+
timed: server.access_log.is_some(),
|
|
504
|
+
wait: Duration::ZERO,
|
|
505
|
+
admitted_at: now,
|
|
506
|
+
gc: None,
|
|
397
507
|
});
|
|
398
508
|
|
|
399
509
|
// Drop guard, not manual decrement: when a client aborts mid-request,
|
|
@@ -472,17 +582,20 @@ async fn handle_request(
|
|
|
472
582
|
}
|
|
473
583
|
};
|
|
474
584
|
|
|
475
|
-
if let (Some(log), Some((start, method, target
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
let
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
585
|
+
if let (Some(log), Some((start, method, target))) = (server.access_log.as_ref(), log_meta) {
|
|
586
|
+
// The worker attached its timing to the response head; a 503 or
|
|
587
|
+
// 504 never reached a worker and carries none.
|
|
588
|
+
let timing = response
|
|
589
|
+
.extensions()
|
|
590
|
+
.get::<crate::access_log::Timing>()
|
|
591
|
+
.copied();
|
|
592
|
+
log.write_line(crate::access_log::completion(
|
|
593
|
+
response.status().as_u16(),
|
|
594
|
+
&method,
|
|
595
|
+
&target,
|
|
596
|
+
start.elapsed(),
|
|
597
|
+
timing,
|
|
598
|
+
));
|
|
486
599
|
}
|
|
487
600
|
|
|
488
601
|
Ok(branded(response))
|
|
@@ -559,7 +672,9 @@ pub fn register_worker(ruby: &Ruby, server_id: u64) -> Result<usize, Error> {
|
|
|
559
672
|
|
|
560
673
|
pub fn stop_accepting(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
561
674
|
if let Some(server) = registry::try_get(server_id) {
|
|
562
|
-
server
|
|
675
|
+
server
|
|
676
|
+
.state
|
|
677
|
+
.store(registry::STATE_DRAINING, Ordering::Relaxed);
|
|
563
678
|
let _ = server.shutdown_tx.send(true);
|
|
564
679
|
}
|
|
565
680
|
Ok(())
|
|
@@ -648,8 +763,11 @@ pub fn interrupt_all_workers(_ruby: &Ruby, server_id: u64) -> Result<(), Error>
|
|
|
648
763
|
|
|
649
764
|
pub fn shutdown_runtime(_ruby: &Ruby, server_id: u64, timeout_ms: u64) -> Result<(), Error> {
|
|
650
765
|
if let Some(server) = registry::remove(server_id) {
|
|
651
|
-
|
|
652
|
-
|
|
766
|
+
let _ = server.shutdown_tx.send(true);
|
|
767
|
+
std::mem::take(&mut *server.runtime.lock()).shutdown(Duration::from_millis(timeout_ms));
|
|
768
|
+
// The listener is closed with the runtime; its socket file is not.
|
|
769
|
+
if let Some(path) = &server.unix_path {
|
|
770
|
+
crate::listen::cleanup_unix(path);
|
|
653
771
|
}
|
|
654
772
|
}
|
|
655
773
|
Ok(())
|
|
@@ -665,12 +783,6 @@ pub fn pin_keeper(
|
|
|
665
783
|
Ok(ruby.obj_wrap(crate::pin::PinKeeper(server.pin_slab.clone())))
|
|
666
784
|
}
|
|
667
785
|
|
|
668
|
-
/// Errors print in red on color terminals. Covers worker errors,
|
|
669
|
-
/// supervisor crash reports, and everything apps write to rack.errors.
|
|
670
|
-
pub fn log_error(message: String) {
|
|
671
|
-
eprintln!("{}", crate::style::red(&format!("[Kino] {message}")));
|
|
672
|
-
}
|
|
673
|
-
|
|
674
786
|
/// Full stats snapshot: [queued, in_flight, served, rejected, timeouts,
|
|
675
787
|
/// respawns, lane_depths]. lane_depths is nil unless lane dispatch is on.
|
|
676
788
|
#[allow(clippy::type_complexity)]
|
|
@@ -709,10 +821,7 @@ pub type WorkerStatRow = (usize, u64, usize, u64, bool);
|
|
|
709
821
|
|
|
710
822
|
/// Per-slot rows for Server#stats parity: [index, served, in_flight,
|
|
711
823
|
/// busy_ms, quarantined] each. Empty when the server is gone.
|
|
712
|
-
pub fn worker_stats(
|
|
713
|
-
_ruby: &Ruby,
|
|
714
|
-
server_id: u64,
|
|
715
|
-
) -> Result<Vec<WorkerStatRow>, Error> {
|
|
824
|
+
pub fn worker_stats(_ruby: &Ruby, server_id: u64) -> Result<Vec<WorkerStatRow>, Error> {
|
|
716
825
|
let Some(server) = registry::try_get(server_id) else {
|
|
717
826
|
return Ok(Vec::new());
|
|
718
827
|
};
|
|
@@ -734,7 +843,9 @@ pub fn quarantine_slot(ruby: &Ruby, server_id: u64, worker_id: usize) -> Result<
|
|
|
734
843
|
/// One replacement spawned by the quarantine monitor.
|
|
735
844
|
pub fn record_quarantine_replacement(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
736
845
|
if let Some(server) = registry::try_get(server_id) {
|
|
737
|
-
server
|
|
846
|
+
server
|
|
847
|
+
.quarantine_replacements
|
|
848
|
+
.fetch_add(1, Ordering::Relaxed);
|
|
738
849
|
}
|
|
739
850
|
Ok(())
|
|
740
851
|
}
|
|
@@ -785,7 +896,10 @@ mod tests {
|
|
|
785
896
|
server.register_worker();
|
|
786
897
|
server.slots.read()[0].lane_tx.lock().take();
|
|
787
898
|
|
|
788
|
-
assert!(matches!(
|
|
899
|
+
assert!(matches!(
|
|
900
|
+
try_dispatch(&server, test_ctx()),
|
|
901
|
+
Dispatch::Closed
|
|
902
|
+
));
|
|
789
903
|
}
|
|
790
904
|
|
|
791
905
|
#[test]
|
|
@@ -828,9 +942,7 @@ mod tests {
|
|
|
828
942
|
let server = test_server(true, 4);
|
|
829
943
|
server.register_worker();
|
|
830
944
|
server.register_worker();
|
|
831
|
-
server.slots.read()[0]
|
|
832
|
-
.parked
|
|
833
|
-
.store(true, Ordering::Relaxed);
|
|
945
|
+
server.slots.read()[0].parked.store(true, Ordering::Relaxed);
|
|
834
946
|
|
|
835
947
|
// Both dispatches land on the awake lane (slot 1), regardless of
|
|
836
948
|
// where the rotating cursor starts.
|
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
|
}
|
data/ext/kino/src/tls.rs
CHANGED
|
@@ -71,7 +71,8 @@ WJ2lRijROyX9v7f8aSQlb6kEwKhI8kG8SbeUc+zbKkzGgRXNaZHY/mAa
|
|
|
71
71
|
#[test]
|
|
72
72
|
fn missing_file_paths_error_without_panicking() {
|
|
73
73
|
let err = build_acceptor("/nonexistent/cert.pem", "/nonexistent/key.pem")
|
|
74
|
-
.err()
|
|
74
|
+
.err()
|
|
75
|
+
.expect("missing files");
|
|
75
76
|
assert!(err.contains("cannot read"));
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -83,14 +84,20 @@ WJ2lRijROyX9v7f8aSQlb6kEwKhI8kG8SbeUc+zbKkzGgRXNaZHY/mAa
|
|
|
83
84
|
|
|
84
85
|
#[test]
|
|
85
86
|
fn pem_without_a_key_is_rejected() {
|
|
86
|
-
let err = build_acceptor(CERT, CERT)
|
|
87
|
+
let err = build_acceptor(CERT, CERT)
|
|
88
|
+
.err()
|
|
89
|
+
.expect("a cert is not a key");
|
|
87
90
|
assert!(err.contains("no private key found"));
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
#[test]
|
|
91
94
|
fn mismatched_cert_and_garbage_key_are_rejected() {
|
|
92
|
-
let err = build_acceptor(
|
|
93
|
-
|
|
95
|
+
let err = build_acceptor(
|
|
96
|
+
CERT,
|
|
97
|
+
"-----BEGIN PRIVATE KEY-----\ngarbage\n-----END PRIVATE KEY-----",
|
|
98
|
+
)
|
|
99
|
+
.err()
|
|
100
|
+
.expect("garbage key");
|
|
94
101
|
assert!(!err.is_empty());
|
|
95
102
|
}
|
|
96
103
|
}
|