kino 0.3.0 → 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 +45 -0
- data/Cargo.lock +133 -4
- data/README.md +56 -14
- 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 +12 -29
- data/ext/kino/src/cpus.rs +37 -0
- data/ext/kino/src/lib.rs +10 -1
- data/ext/kino/src/listen.rs +134 -0
- data/ext/kino/src/log.rs +144 -0
- data/ext/kino/src/queue.rs +7 -3
- data/ext/kino/src/registry.rs +3 -0
- data/ext/kino/src/request.rs +48 -2
- data/ext/kino/src/server.rs +114 -48
- data/ext/kino/src/style.rs +42 -39
- data/lib/kino/cli.rb +20 -9
- data/lib/kino/configuration.rb +20 -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 +60 -15
- data/lib/kino/templates/kino.rb.tt +11 -5
- 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 +62 -1
- metadata +21 -1
data/ext/kino/src/control.rs
CHANGED
|
@@ -300,32 +300,9 @@ pub enum ControlBind {
|
|
|
300
300
|
/// Claim the control address. Both arms bind synchronously so a
|
|
301
301
|
/// conflict raises at boot, like the main listener.
|
|
302
302
|
pub fn bind_control(addr: &str) -> std::io::Result<ControlBind> {
|
|
303
|
-
if let Some(path) = addr
|
|
304
|
-
let
|
|
305
|
-
|
|
306
|
-
// not steal it) or a stale file left behind by a crashed process
|
|
307
|
-
// (safe to unlink and reclaim). Probe with a connect: a successful
|
|
308
|
-
// connect means someone is accepting on it right now.
|
|
309
|
-
match std::os::unix::net::UnixStream::connect(&path) {
|
|
310
|
-
Ok(_) => {
|
|
311
|
-
return Err(std::io::Error::new(
|
|
312
|
-
std::io::ErrorKind::AddrInUse,
|
|
313
|
-
"control socket is in use",
|
|
314
|
-
));
|
|
315
|
-
}
|
|
316
|
-
Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => {
|
|
317
|
-
match std::fs::remove_file(&path) {
|
|
318
|
-
Ok(()) => {}
|
|
319
|
-
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
|
320
|
-
Err(e) => return Err(e),
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
|
324
|
-
Err(e) => return Err(e),
|
|
325
|
-
}
|
|
326
|
-
let listener = std::os::unix::net::UnixListener::bind(&path)?;
|
|
327
|
-
listener.set_nonblocking(true)?;
|
|
328
|
-
Ok(ControlBind::Unix(listener, path))
|
|
303
|
+
if let Some(path) = crate::listen::unix_path(addr) {
|
|
304
|
+
let listener = crate::listen::bind_unix(path)?;
|
|
305
|
+
Ok(ControlBind::Unix(listener, path.to_path_buf()))
|
|
329
306
|
} else {
|
|
330
307
|
let listener = std::net::TcpListener::bind(addr)?;
|
|
331
308
|
listener.set_nonblocking(true)?;
|
|
@@ -405,22 +382,28 @@ fn run(
|
|
|
405
382
|
) {
|
|
406
383
|
let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
|
|
407
384
|
Ok(runtime) => runtime,
|
|
408
|
-
Err(e) => return
|
|
385
|
+
Err(e) => return native_error(format!("control runtime failed: {e}")),
|
|
409
386
|
};
|
|
410
387
|
runtime.block_on(async move {
|
|
411
388
|
match bind {
|
|
412
389
|
ControlBind::Tcp(listener, _) => match tokio::net::TcpListener::from_std(listener) {
|
|
413
390
|
Ok(listener) => serve(TcpOrUnix::Tcp(listener), server, token, stop_rx).await,
|
|
414
|
-
Err(e) =>
|
|
391
|
+
Err(e) => native_error(format!("control listener failed: {e}")),
|
|
415
392
|
},
|
|
416
393
|
ControlBind::Unix(listener, _) => match tokio::net::UnixListener::from_std(listener) {
|
|
417
394
|
Ok(listener) => serve(TcpOrUnix::Unix(listener), server, token, stop_rx).await,
|
|
418
|
-
Err(e) =>
|
|
395
|
+
Err(e) => native_error(format!("control listener failed: {e}")),
|
|
419
396
|
},
|
|
420
397
|
}
|
|
421
398
|
});
|
|
422
399
|
}
|
|
423
400
|
|
|
401
|
+
/// A failure inside the native layer itself, reported as the "native"
|
|
402
|
+
/// source: no Ruby ractor or thread spoke.
|
|
403
|
+
fn native_error(message: String) {
|
|
404
|
+
crate::log::emit(crate::log::Level::Error, "native", &message);
|
|
405
|
+
}
|
|
406
|
+
|
|
424
407
|
enum TcpOrUnix {
|
|
425
408
|
Tcp(tokio::net::TcpListener),
|
|
426
409
|
Unix(tokio::net::UnixListener),
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//! How many CPUs this process may actually use: the default worker count.
|
|
2
|
+
//!
|
|
3
|
+
//! `Etc.nprocessors` honours the affinity mask but not a cgroup CPU quota,
|
|
4
|
+
//! so a container limited to two CPUs on a 64-core host would spawn 64
|
|
5
|
+
//! workers. The standard library's `available_parallelism` reads both the
|
|
6
|
+
//! mask and the cgroup v1/v2 quota on Linux (rounding a fractional quota
|
|
7
|
+
//! up), which is also what tokio sizes its own pool by.
|
|
8
|
+
|
|
9
|
+
use magnus::{Error, Ruby};
|
|
10
|
+
|
|
11
|
+
/// The usable CPU count, never below one (a quota of 0.5 CPU still needs
|
|
12
|
+
/// a worker; an unreadable count falls back to one rather than failing
|
|
13
|
+
/// boot).
|
|
14
|
+
pub fn available_parallelism(_ruby: &Ruby) -> Result<usize, Error> {
|
|
15
|
+
Ok(count())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn count() -> usize {
|
|
19
|
+
std::thread::available_parallelism().map_or(1, |n| n.get())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[cfg(test)]
|
|
23
|
+
mod tests {
|
|
24
|
+
use super::count;
|
|
25
|
+
|
|
26
|
+
#[test]
|
|
27
|
+
fn reports_at_least_one_cpu() {
|
|
28
|
+
assert!(count() >= 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn never_exceeds_what_the_os_reports_as_online() {
|
|
33
|
+
// A quota can only lower the count below the online CPUs; never raise it.
|
|
34
|
+
let online = std::thread::available_parallelism().map_or(1, |n| n.get());
|
|
35
|
+
assert!(count() <= online);
|
|
36
|
+
}
|
|
37
|
+
}
|
data/ext/kino/src/lib.rs
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
#[global_allocator]
|
|
5
5
|
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
6
6
|
|
|
7
|
+
mod access_log;
|
|
7
8
|
mod control;
|
|
9
|
+
mod cpus;
|
|
8
10
|
mod env_strings;
|
|
9
11
|
mod gvl;
|
|
12
|
+
mod listen;
|
|
13
|
+
mod log;
|
|
10
14
|
mod logsink;
|
|
11
15
|
mod mono;
|
|
12
16
|
mod pin;
|
|
@@ -62,8 +66,12 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
62
66
|
function!(server::interrupt_all_workers, 1),
|
|
63
67
|
)?;
|
|
64
68
|
native.define_singleton_method("shutdown_runtime", function!(server::shutdown_runtime, 2))?;
|
|
65
|
-
native.define_singleton_method("
|
|
69
|
+
native.define_singleton_method("log_line", function!(log::log_line, 3))?;
|
|
66
70
|
native.define_singleton_method("sleep_chunk", function!(timer::sleep_chunk, 1))?;
|
|
71
|
+
native.define_singleton_method(
|
|
72
|
+
"available_parallelism",
|
|
73
|
+
function!(cpus::available_parallelism, 0),
|
|
74
|
+
)?;
|
|
67
75
|
native.define_singleton_method("log_device_open", function!(logsink::device_open, 1))?;
|
|
68
76
|
native.define_singleton_method("log_device_write", function!(logsink::device_write, 2))?;
|
|
69
77
|
native.define_singleton_method("log_device_close", function!(logsink::device_close, 1))?;
|
|
@@ -87,6 +95,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
87
95
|
request.define_method("write_chunk", method!(Request::write_chunk, 1))?;
|
|
88
96
|
request.define_method("finish", method!(Request::finish, 0))?;
|
|
89
97
|
request.define_method("abort", method!(Request::abort, 0))?;
|
|
98
|
+
request.define_method("timing", method!(Request::set_timing, 2))?;
|
|
90
99
|
|
|
91
100
|
// Force-resolve the TypedData class cache on the main ractor: magnus
|
|
92
101
|
// resolves it lazily on first wrap, and a racy first resolution from two
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
//! Listening sockets for the main server and the control plane: a TCP
|
|
2
|
+
//! `host:port`, or a `unix://path` domain socket (the usual shape behind
|
|
3
|
+
//! nginx). Binding is synchronous so an address conflict surfaces at boot.
|
|
4
|
+
|
|
5
|
+
use std::io;
|
|
6
|
+
use std::os::unix::net::UnixListener;
|
|
7
|
+
use std::path::{Path, PathBuf};
|
|
8
|
+
|
|
9
|
+
/// The bind scheme that selects a unix domain socket; anything else is a
|
|
10
|
+
/// TCP host.
|
|
11
|
+
pub const UNIX_SCHEME: &str = "unix://";
|
|
12
|
+
|
|
13
|
+
/// The socket path of a `unix://` bind, or None for a TCP host.
|
|
14
|
+
pub fn unix_path(bind: &str) -> Option<&Path> {
|
|
15
|
+
bind.strip_prefix(UNIX_SCHEME).map(Path::new)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// A bound, non-blocking listener of either kind.
|
|
19
|
+
pub enum Listener {
|
|
20
|
+
Tcp(std::net::TcpListener),
|
|
21
|
+
Unix(UnixListener, PathBuf),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl Listener {
|
|
25
|
+
/// Bind `bind:port` (TCP; a hostname resolves to its addresses and the
|
|
26
|
+
/// first that binds wins) or `unix://path`.
|
|
27
|
+
pub fn bind(bind: &str, port: u16) -> io::Result<Listener> {
|
|
28
|
+
match unix_path(bind) {
|
|
29
|
+
Some(path) => Ok(Listener::Unix(bind_unix(path)?, path.to_path_buf())),
|
|
30
|
+
None => {
|
|
31
|
+
let listener = std::net::TcpListener::bind((bind, port))?;
|
|
32
|
+
listener.set_nonblocking(true)?;
|
|
33
|
+
Ok(Listener::Tcp(listener))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// The bound TCP port; 0 for a unix socket, which has none.
|
|
39
|
+
pub fn port(&self) -> io::Result<u16> {
|
|
40
|
+
match self {
|
|
41
|
+
Listener::Tcp(listener) => Ok(listener.local_addr()?.port()),
|
|
42
|
+
Listener::Unix(..) => Ok(0),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Bind a unix domain socket at `path`. A path that already exists is
|
|
48
|
+
/// either a live listener (refuse: never steal it) or a stale file left
|
|
49
|
+
/// behind by a crashed process (unlink and reclaim). A connect probe tells
|
|
50
|
+
/// them apart: a successful connect means someone is accepting right now.
|
|
51
|
+
pub fn bind_unix(path: &Path) -> io::Result<UnixListener> {
|
|
52
|
+
match std::os::unix::net::UnixStream::connect(path) {
|
|
53
|
+
Ok(_) => return Err(io::Error::new(io::ErrorKind::AddrInUse, "socket is in use")),
|
|
54
|
+
Err(e) if e.kind() == io::ErrorKind::ConnectionRefused => {
|
|
55
|
+
match std::fs::remove_file(path) {
|
|
56
|
+
Ok(()) => {}
|
|
57
|
+
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
|
|
58
|
+
Err(e) => return Err(e),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
|
|
62
|
+
Err(e) => return Err(e),
|
|
63
|
+
}
|
|
64
|
+
let listener = UnixListener::bind(path)?;
|
|
65
|
+
listener.set_nonblocking(true)?;
|
|
66
|
+
Ok(listener)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Remove a socket file at shutdown; one that is already gone is fine.
|
|
70
|
+
pub fn cleanup_unix(path: &Path) {
|
|
71
|
+
let _ = std::fs::remove_file(path);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[cfg(test)]
|
|
75
|
+
mod tests {
|
|
76
|
+
use super::{bind_unix, unix_path, Listener};
|
|
77
|
+
use std::path::PathBuf;
|
|
78
|
+
|
|
79
|
+
/// A socket path unique to this process and test. macOS caps sun_path
|
|
80
|
+
/// at 104 bytes, so the name stays short.
|
|
81
|
+
fn socket_path(name: &str) -> PathBuf {
|
|
82
|
+
let path = std::env::temp_dir().join(format!("kino-{}-{name}.sock", std::process::id()));
|
|
83
|
+
let _ = std::fs::remove_file(&path);
|
|
84
|
+
path
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn unix_path_recognises_only_the_unix_scheme() {
|
|
89
|
+
assert_eq!(unix_path("unix:///run/kino.sock").unwrap().to_str(), Some("/run/kino.sock"));
|
|
90
|
+
assert!(unix_path("127.0.0.1").is_none());
|
|
91
|
+
assert!(unix_path("unix.example.com").is_none());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[test]
|
|
95
|
+
fn binds_a_unix_socket_and_reports_no_port() {
|
|
96
|
+
let path = socket_path("bind");
|
|
97
|
+
let listener = Listener::bind(&format!("unix://{}", path.display()), 9292).unwrap();
|
|
98
|
+
assert!(matches!(listener, Listener::Unix(..)));
|
|
99
|
+
assert_eq!(listener.port().unwrap(), 0);
|
|
100
|
+
assert!(std::fs::metadata(&path).is_ok());
|
|
101
|
+
drop(listener);
|
|
102
|
+
let _ = std::fs::remove_file(&path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn reclaims_a_stale_socket_file() {
|
|
107
|
+
let path = socket_path("stale");
|
|
108
|
+
// Dropping a listener closes the socket but leaves its file behind,
|
|
109
|
+
// exactly what a crashed process leaves.
|
|
110
|
+
drop(bind_unix(&path).unwrap());
|
|
111
|
+
assert!(std::fs::metadata(&path).is_ok());
|
|
112
|
+
let reclaimed = bind_unix(&path).unwrap();
|
|
113
|
+
drop(reclaimed);
|
|
114
|
+
let _ = std::fs::remove_file(&path);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[test]
|
|
118
|
+
fn refuses_a_socket_someone_is_listening_on() {
|
|
119
|
+
let path = socket_path("live");
|
|
120
|
+
let live = bind_unix(&path).unwrap();
|
|
121
|
+
let err = bind_unix(&path).unwrap_err();
|
|
122
|
+
assert_eq!(err.kind(), std::io::ErrorKind::AddrInUse);
|
|
123
|
+
assert!(err.to_string().contains("in use"));
|
|
124
|
+
drop(live);
|
|
125
|
+
let _ = std::fs::remove_file(&path);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[test]
|
|
129
|
+
fn binds_tcp_on_an_ephemeral_port() {
|
|
130
|
+
let listener = Listener::bind("127.0.0.1", 0).unwrap();
|
|
131
|
+
assert!(matches!(listener, Listener::Tcp(_)));
|
|
132
|
+
assert_ne!(listener.port().unwrap(), 0);
|
|
133
|
+
}
|
|
134
|
+
}
|
data/ext/kino/src/log.rs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
//! Server log lines: lifecycle notices, crashes and respawns, hook
|
|
2
|
+
//! failures, the failed-request report, and whatever apps write to
|
|
3
|
+
//! rack.errors, all in one shape:
|
|
4
|
+
//!
|
|
5
|
+
//! ```text
|
|
6
|
+
//! kino[4213] worker-3: after_worker_boot hook raised RuntimeError: boom
|
|
7
|
+
//! ```
|
|
8
|
+
//!
|
|
9
|
+
//! The label is syslog's `ident[pid]` tag plus the source that spoke (the
|
|
10
|
+
//! ractor and/or thread name, `main` for neither), styled by level; the
|
|
11
|
+
//! message stays plain. Ruby builds the source, since only Ruby knows its
|
|
12
|
+
//! ractor and thread names, and hands the rest over; this side decides
|
|
13
|
+
//! color per stream and writes, so worker ractors never touch $stdout or
|
|
14
|
+
//! $stderr themselves. A multi-line message is labelled on its first line.
|
|
15
|
+
|
|
16
|
+
use std::io::Write;
|
|
17
|
+
|
|
18
|
+
use magnus::{Error, Ruby};
|
|
19
|
+
|
|
20
|
+
use crate::style::{self, Stream};
|
|
21
|
+
|
|
22
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
23
|
+
pub enum Level {
|
|
24
|
+
Info,
|
|
25
|
+
Warn,
|
|
26
|
+
Error,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Level {
|
|
30
|
+
/// The level named by Ruby ("info", "warn", "error").
|
|
31
|
+
pub fn parse(name: &str) -> Option<Level> {
|
|
32
|
+
match name {
|
|
33
|
+
"info" => Some(Level::Info),
|
|
34
|
+
"warn" => Some(Level::Warn),
|
|
35
|
+
"error" => Some(Level::Error),
|
|
36
|
+
_ => None,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Notes go to stdout; warnings and errors to stderr.
|
|
41
|
+
fn stream(self) -> Stream {
|
|
42
|
+
match self {
|
|
43
|
+
Level::Info => Stream::Stdout,
|
|
44
|
+
Level::Warn | Level::Error => Stream::Stderr,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn sgr(self) -> &'static str {
|
|
49
|
+
match self {
|
|
50
|
+
Level::Info => style::DIM,
|
|
51
|
+
Level::Warn => style::WARN,
|
|
52
|
+
Level::Error => style::ERROR,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Write one line from `source` at `level`.
|
|
58
|
+
pub fn emit(level: Level, source: &str, message: &str) {
|
|
59
|
+
let stream = level.stream();
|
|
60
|
+
let label = label(std::process::id(), source);
|
|
61
|
+
let line = format_line(level, &label, message, style::enabled(stream));
|
|
62
|
+
match stream {
|
|
63
|
+
Stream::Stdout => {
|
|
64
|
+
let _ = writeln!(std::io::stdout().lock(), "{line}");
|
|
65
|
+
}
|
|
66
|
+
Stream::Stderr => {
|
|
67
|
+
let _ = writeln!(std::io::stderr().lock(), "{line}");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// The `kino[<pid>] <source>:` tag.
|
|
73
|
+
pub fn label(pid: u32, source: &str) -> String {
|
|
74
|
+
format!("kino[{pid}] {source}:")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// The label styled by level, then the message as given.
|
|
78
|
+
pub fn format_line(level: Level, label: &str, message: &str, color: bool) -> String {
|
|
79
|
+
format!("{} {message}", style::sgr(level.sgr(), label, color))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// The Ruby entry point (Kino::Log): Ruby knows its ractor and thread,
|
|
83
|
+
/// the native side knows the terminal.
|
|
84
|
+
pub fn log_line(ruby: &Ruby, level: String, source: String, message: String) -> Result<(), Error> {
|
|
85
|
+
let level = Level::parse(&level).ok_or_else(|| {
|
|
86
|
+
Error::new(
|
|
87
|
+
ruby.exception_arg_error(),
|
|
88
|
+
format!("unknown log level {level:?}"),
|
|
89
|
+
)
|
|
90
|
+
})?;
|
|
91
|
+
emit(level, &source, &message);
|
|
92
|
+
Ok(())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[cfg(test)]
|
|
96
|
+
mod tests {
|
|
97
|
+
use super::{format_line, label, Level};
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn label_is_a_syslog_tag_plus_the_source() {
|
|
101
|
+
assert_eq!(label(4213, "main"), "kino[4213] main:");
|
|
102
|
+
assert_eq!(label(4213, "worker-3/thread-2"), "kino[4213] worker-3/thread-2:");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn plain_line_is_label_then_message() {
|
|
107
|
+
assert_eq!(
|
|
108
|
+
format_line(Level::Info, "kino[1] main:", "hello", false),
|
|
109
|
+
"kino[1] main: hello"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn color_styles_only_the_label_by_level() {
|
|
115
|
+
assert_eq!(
|
|
116
|
+
format_line(Level::Info, "kino[1] main:", "hello", true),
|
|
117
|
+
"\x1b[90mkino[1] main:\x1b[0m hello"
|
|
118
|
+
);
|
|
119
|
+
assert_eq!(
|
|
120
|
+
format_line(Level::Warn, "kino[1] main:", "careful", true),
|
|
121
|
+
"\x1b[33mkino[1] main:\x1b[0m careful"
|
|
122
|
+
);
|
|
123
|
+
assert_eq!(
|
|
124
|
+
format_line(Level::Error, "kino[1] main:", "broke", true),
|
|
125
|
+
"\x1b[91mkino[1] main:\x1b[0m broke"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn a_report_is_labelled_on_its_first_line_only() {
|
|
131
|
+
assert_eq!(
|
|
132
|
+
format_line(Level::Error, "kino[1] main:", "500 GET / · X: y\n a.rb:1", false),
|
|
133
|
+
"kino[1] main: 500 GET / · X: y\n a.rb:1"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[test]
|
|
138
|
+
fn levels_parse_from_their_ruby_names() {
|
|
139
|
+
assert_eq!(Level::parse("info"), Some(Level::Info));
|
|
140
|
+
assert_eq!(Level::parse("warn"), Some(Level::Warn));
|
|
141
|
+
assert_eq!(Level::parse("error"), Some(Level::Error));
|
|
142
|
+
assert_eq!(Level::parse("debug"), None);
|
|
143
|
+
}
|
|
144
|
+
}
|
data/ext/kino/src/queue.rs
CHANGED
|
@@ -119,9 +119,13 @@ fn admit(
|
|
|
119
119
|
slot.served.fetch_add(1, Ordering::Relaxed);
|
|
120
120
|
slot.last_started_ms.store(crate::mono::mono_ms(), Ordering::Relaxed);
|
|
121
121
|
slot.in_flight.fetch_add(1, Ordering::Relaxed);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
// One clock read serves the histogram and, for the access log, the
|
|
123
|
+
// request's queue wait and the start of its time in Ruby.
|
|
124
|
+
let now = std::time::Instant::now();
|
|
125
|
+
let wait = now.duration_since(ctx.enqueued_at);
|
|
126
|
+
server.queue_histogram.record(wait.as_micros() as u64);
|
|
127
|
+
ctx.wait = wait;
|
|
128
|
+
ctx.admitted_at = now;
|
|
125
129
|
slot.current.lock().push(Arc::downgrade(&ctx.responder));
|
|
126
130
|
// Wire the slot into the request so blocked body reads/writes are
|
|
127
131
|
// interruptible the same way the queue pop is.
|
data/ext/kino/src/registry.rs
CHANGED
|
@@ -69,6 +69,8 @@ pub struct ServerInner {
|
|
|
69
69
|
pub quarantine_replacements: AtomicU64,
|
|
70
70
|
pub topology: Topology,
|
|
71
71
|
pub https: bool,
|
|
72
|
+
/// The socket file of a `unix://` bind, removed at shutdown.
|
|
73
|
+
pub unix_path: Option<std::path::PathBuf>,
|
|
72
74
|
/// Native access log sink (None unless log_requests is on).
|
|
73
75
|
pub access_log: Option<crate::logsink::Sink>,
|
|
74
76
|
/// Lane-dispatch mode: per-worker queues, awake-preferring dispatch.
|
|
@@ -301,6 +303,7 @@ pub fn test_server(lanes: bool, queue_depth: usize) -> Arc<ServerInner> {
|
|
|
301
303
|
quarantine_replacements: AtomicU64::new(0),
|
|
302
304
|
topology: Topology { mode: "threaded".to_string(), workers: 0, threads: 0, batch: 1 },
|
|
303
305
|
https: false,
|
|
306
|
+
unix_path: None,
|
|
304
307
|
access_log: None,
|
|
305
308
|
lanes,
|
|
306
309
|
lane_cursor: AtomicUsize::new(0),
|
data/ext/kino/src/request.rs
CHANGED
|
@@ -44,6 +44,41 @@ pub struct RequestCtx {
|
|
|
44
44
|
/// When this request entered the queue, for the queue-wait histogram.
|
|
45
45
|
/// Stamped at ctx creation; read once at admit (queue.rs).
|
|
46
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
|
+
}
|
|
47
82
|
}
|
|
48
83
|
|
|
49
84
|
impl Drop for RequestCtx {
|
|
@@ -233,7 +268,7 @@ pub fn respond_simple(
|
|
|
233
268
|
body: RString,
|
|
234
269
|
) -> Result<bool, Error> {
|
|
235
270
|
let ctx = request.0.borrow();
|
|
236
|
-
let builder = build_head(status, headers)
|
|
271
|
+
let builder = timed(&ctx, build_head(status, headers)?);
|
|
237
272
|
let bytes = body_bytes(&ctx, body);
|
|
238
273
|
let response = builder
|
|
239
274
|
.body(full_body(bytes))
|
|
@@ -252,6 +287,13 @@ fn body_bytes(ctx: &RequestCtx, body: RString) -> Bytes {
|
|
|
252
287
|
}
|
|
253
288
|
|
|
254
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
|
+
|
|
255
297
|
/// Next chunk of the request body, at most `max_len` bytes; nil at EOF.
|
|
256
298
|
/// Blocks (GVL released) until the client sends more.
|
|
257
299
|
pub fn read_body(
|
|
@@ -306,7 +348,7 @@ impl Request {
|
|
|
306
348
|
headers: RHash,
|
|
307
349
|
) -> Result<bool, Error> {
|
|
308
350
|
let ctx = rb_self.0.borrow();
|
|
309
|
-
let builder = build_head(status, headers)
|
|
351
|
+
let builder = timed(&ctx, build_head(status, headers)?);
|
|
310
352
|
ctx.responder
|
|
311
353
|
.send_stream_head(builder)
|
|
312
354
|
.map_err(|e| invalid_response(ruby, e))
|
|
@@ -429,6 +471,10 @@ pub fn test_ctx() -> crate::registry::BoxedCtx {
|
|
|
429
471
|
pin_slab: Arc::new(crate::pin::PinSlab::new()),
|
|
430
472
|
responder: Arc::new(Responder::new(head_tx)),
|
|
431
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,
|
|
432
478
|
})
|
|
433
479
|
}
|
|
434
480
|
|