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/control.rs
CHANGED
|
@@ -120,7 +120,11 @@ pub fn stats_json(s: &StatsSnapshot) -> String {
|
|
|
120
120
|
)
|
|
121
121
|
.expect("writing to a String cannot fail");
|
|
122
122
|
if let Some(depths) = &s.lane_depths {
|
|
123
|
-
let list = depths
|
|
123
|
+
let list = depths
|
|
124
|
+
.iter()
|
|
125
|
+
.map(|d| d.to_string())
|
|
126
|
+
.collect::<Vec<_>>()
|
|
127
|
+
.join(",");
|
|
124
128
|
write!(out, r#","lane_depths":[{list}]"#).expect("writing to a String cannot fail");
|
|
125
129
|
}
|
|
126
130
|
out.push_str(r#","worker_status":["#);
|
|
@@ -146,7 +150,8 @@ pub fn stats_json(s: &StatsSnapshot) -> String {
|
|
|
146
150
|
write!(
|
|
147
151
|
out,
|
|
148
152
|
r#","quarantined":{},"state":"{}","version":"{}"}}"#,
|
|
149
|
-
s.quarantined_count,
|
|
153
|
+
s.quarantined_count,
|
|
154
|
+
s.state_name(),
|
|
150
155
|
env!("CARGO_PKG_VERSION")
|
|
151
156
|
)
|
|
152
157
|
.expect("writing to a String cannot fail");
|
|
@@ -156,8 +161,11 @@ pub fn stats_json(s: &StatsSnapshot) -> String {
|
|
|
156
161
|
/// One HELP/TYPE/sample triple for a single-value metric.
|
|
157
162
|
fn metric(out: &mut String, name: &str, kind: &str, help: &str, value: impl std::fmt::Display) {
|
|
158
163
|
use std::fmt::Write;
|
|
159
|
-
writeln!(
|
|
160
|
-
|
|
164
|
+
writeln!(
|
|
165
|
+
out,
|
|
166
|
+
"# HELP {name} {help}\n# TYPE {name} {kind}\n{name} {value}"
|
|
167
|
+
)
|
|
168
|
+
.expect("writing to a String cannot fail");
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
/// One HELP/TYPE header followed by one `name{label="key"} value` line per
|
|
@@ -171,9 +179,11 @@ fn series<K: std::fmt::Display, V: std::fmt::Display>(
|
|
|
171
179
|
rows: impl Iterator<Item = (K, V)>,
|
|
172
180
|
) {
|
|
173
181
|
use std::fmt::Write;
|
|
174
|
-
writeln!(out, "# HELP {name} {help}\n# TYPE {name} {kind}")
|
|
182
|
+
writeln!(out, "# HELP {name} {help}\n# TYPE {name} {kind}")
|
|
183
|
+
.expect("writing to a String cannot fail");
|
|
175
184
|
for (key, value) in rows {
|
|
176
|
-
writeln!(out, "{name}{{{label}=\"{key}\"}} {value}")
|
|
185
|
+
writeln!(out, "{name}{{{label}=\"{key}\"}} {value}")
|
|
186
|
+
.expect("writing to a String cannot fail");
|
|
177
187
|
}
|
|
178
188
|
}
|
|
179
189
|
|
|
@@ -181,40 +191,138 @@ fn series<K: std::fmt::Display, V: std::fmt::Display>(
|
|
|
181
191
|
pub fn metrics_text(s: &StatsSnapshot) -> String {
|
|
182
192
|
use std::fmt::Write;
|
|
183
193
|
let mut out = String::with_capacity(1024);
|
|
184
|
-
metric(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
metric(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
metric(
|
|
195
|
+
&mut out,
|
|
196
|
+
"kino_requests_served_total",
|
|
197
|
+
"counter",
|
|
198
|
+
"Requests handed to Ruby workers.",
|
|
199
|
+
s.served,
|
|
200
|
+
);
|
|
201
|
+
metric(
|
|
202
|
+
&mut out,
|
|
203
|
+
"kino_requests_rejected_total",
|
|
204
|
+
"counter",
|
|
205
|
+
"Requests rejected with a 503.",
|
|
206
|
+
s.rejected,
|
|
207
|
+
);
|
|
208
|
+
metric(
|
|
209
|
+
&mut out,
|
|
210
|
+
"kino_request_timeouts_total",
|
|
211
|
+
"counter",
|
|
212
|
+
"Responses past the request timeout (client got a 504).",
|
|
213
|
+
s.timeouts,
|
|
214
|
+
);
|
|
215
|
+
metric(
|
|
216
|
+
&mut out,
|
|
217
|
+
"kino_worker_respawns_total",
|
|
218
|
+
"counter",
|
|
219
|
+
"Crashed workers respawned by the supervisor.",
|
|
220
|
+
s.respawns,
|
|
221
|
+
);
|
|
222
|
+
metric(
|
|
223
|
+
&mut out,
|
|
224
|
+
"kino_queue_depth",
|
|
225
|
+
"gauge",
|
|
226
|
+
"Requests waiting for a worker.",
|
|
227
|
+
s.queued,
|
|
228
|
+
);
|
|
229
|
+
metric(
|
|
230
|
+
&mut out,
|
|
231
|
+
"kino_requests_in_flight",
|
|
232
|
+
"gauge",
|
|
233
|
+
"Requests currently inside Ruby workers.",
|
|
234
|
+
s.in_flight,
|
|
235
|
+
);
|
|
236
|
+
metric(
|
|
237
|
+
&mut out,
|
|
238
|
+
"kino_workers",
|
|
239
|
+
"gauge",
|
|
240
|
+
"Configured worker count.",
|
|
241
|
+
s.workers,
|
|
242
|
+
);
|
|
243
|
+
metric(
|
|
244
|
+
&mut out,
|
|
245
|
+
"kino_threads_per_worker",
|
|
246
|
+
"gauge",
|
|
247
|
+
"Configured threads per worker.",
|
|
248
|
+
s.threads,
|
|
249
|
+
);
|
|
250
|
+
metric(
|
|
251
|
+
&mut out,
|
|
252
|
+
"kino_ready",
|
|
253
|
+
"gauge",
|
|
254
|
+
"1 when serving, 0 while booting or draining.",
|
|
255
|
+
if s.state == STATE_READY { "1" } else { "0" },
|
|
256
|
+
);
|
|
194
257
|
if let Some(depths) = &s.lane_depths {
|
|
195
|
-
series(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
258
|
+
series(
|
|
259
|
+
&mut out,
|
|
260
|
+
"kino_lane_depth",
|
|
261
|
+
"gauge",
|
|
262
|
+
"Queued requests in each worker lane.",
|
|
263
|
+
"lane",
|
|
264
|
+
depths
|
|
265
|
+
.iter()
|
|
266
|
+
.enumerate()
|
|
267
|
+
.map(|(lane, depth)| (lane, *depth)),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
series(
|
|
271
|
+
&mut out,
|
|
272
|
+
"kino_worker_requests_served_total",
|
|
273
|
+
"counter",
|
|
274
|
+
"Requests handed to each dispatch slot.",
|
|
275
|
+
"worker",
|
|
276
|
+
s.worker_status.iter().map(|w| (w.index, w.served)),
|
|
277
|
+
);
|
|
278
|
+
series(
|
|
279
|
+
&mut out,
|
|
280
|
+
"kino_worker_in_flight",
|
|
281
|
+
"gauge",
|
|
282
|
+
"Requests executing in each dispatch slot.",
|
|
283
|
+
"worker",
|
|
284
|
+
s.worker_status.iter().map(|w| (w.index, w.in_flight)),
|
|
285
|
+
);
|
|
286
|
+
series(
|
|
287
|
+
&mut out,
|
|
288
|
+
"kino_worker_busy_ms",
|
|
289
|
+
"gauge",
|
|
290
|
+
"Age in ms of the current in-flight request per slot (0 when idle).",
|
|
291
|
+
"worker",
|
|
292
|
+
s.worker_status.iter().map(|w| (w.index, w.busy_ms)),
|
|
293
|
+
);
|
|
294
|
+
metric(
|
|
295
|
+
&mut out,
|
|
296
|
+
"kino_quarantined_workers",
|
|
297
|
+
"gauge",
|
|
298
|
+
"Dispatch slots abandoned as wedged.",
|
|
299
|
+
s.quarantined_count,
|
|
300
|
+
);
|
|
301
|
+
metric(
|
|
302
|
+
&mut out,
|
|
303
|
+
"kino_quarantine_replacements_total",
|
|
304
|
+
"counter",
|
|
305
|
+
"Replacement workers spawned after a wedge.",
|
|
306
|
+
s.quarantine_replacements,
|
|
307
|
+
);
|
|
206
308
|
let h = &s.queue_histogram;
|
|
207
309
|
out.push_str("# HELP kino_request_queue_seconds Seconds requests waited in the queue before a worker admitted them.\n# TYPE kino_request_queue_seconds histogram\n");
|
|
208
310
|
let mut cumulative = 0u64;
|
|
209
311
|
for (i, bound_us) in crate::registry::QUEUE_BOUNDS_US.iter().enumerate() {
|
|
210
312
|
cumulative += h.buckets[i];
|
|
211
313
|
let le = *bound_us as f64 / 1_000_000.0;
|
|
212
|
-
writeln!(
|
|
213
|
-
|
|
314
|
+
writeln!(
|
|
315
|
+
out,
|
|
316
|
+
"kino_request_queue_seconds_bucket{{le=\"{le}\"}} {cumulative}"
|
|
317
|
+
)
|
|
318
|
+
.expect("writing to a String cannot fail");
|
|
214
319
|
}
|
|
215
320
|
let total = cumulative + h.overflow;
|
|
216
|
-
writeln!(
|
|
217
|
-
|
|
321
|
+
writeln!(
|
|
322
|
+
out,
|
|
323
|
+
"kino_request_queue_seconds_bucket{{le=\"+Inf\"}} {total}"
|
|
324
|
+
)
|
|
325
|
+
.expect("writing to a String cannot fail");
|
|
218
326
|
writeln!(out, "kino_request_queue_seconds_sum {}", h.sum_seconds())
|
|
219
327
|
.expect("writing to a String cannot fail");
|
|
220
328
|
writeln!(out, "kino_request_queue_seconds_count {total}")
|
|
@@ -300,32 +408,9 @@ pub enum ControlBind {
|
|
|
300
408
|
/// Claim the control address. Both arms bind synchronously so a
|
|
301
409
|
/// conflict raises at boot, like the main listener.
|
|
302
410
|
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))
|
|
411
|
+
if let Some(path) = crate::listen::unix_path(addr) {
|
|
412
|
+
let listener = crate::listen::bind_unix(path)?;
|
|
413
|
+
Ok(ControlBind::Unix(listener, path.to_path_buf()))
|
|
329
414
|
} else {
|
|
330
415
|
let listener = std::net::TcpListener::bind(addr)?;
|
|
331
416
|
listener.set_nonblocking(true)?;
|
|
@@ -377,7 +462,11 @@ pub fn start(
|
|
|
377
462
|
};
|
|
378
463
|
control_registry().lock().insert(
|
|
379
464
|
id,
|
|
380
|
-
ControlHandle {
|
|
465
|
+
ControlHandle {
|
|
466
|
+
stop_tx,
|
|
467
|
+
join,
|
|
468
|
+
unix_path,
|
|
469
|
+
},
|
|
381
470
|
);
|
|
382
471
|
Ok(port)
|
|
383
472
|
}
|
|
@@ -403,24 +492,33 @@ fn run(
|
|
|
403
492
|
token: Option<String>,
|
|
404
493
|
stop_rx: tokio::sync::watch::Receiver<bool>,
|
|
405
494
|
) {
|
|
406
|
-
let runtime = match tokio::runtime::Builder::new_current_thread()
|
|
495
|
+
let runtime = match tokio::runtime::Builder::new_current_thread()
|
|
496
|
+
.enable_all()
|
|
497
|
+
.build()
|
|
498
|
+
{
|
|
407
499
|
Ok(runtime) => runtime,
|
|
408
|
-
Err(e) => return
|
|
500
|
+
Err(e) => return native_error(format!("control runtime failed: {e}")),
|
|
409
501
|
};
|
|
410
502
|
runtime.block_on(async move {
|
|
411
503
|
match bind {
|
|
412
504
|
ControlBind::Tcp(listener, _) => match tokio::net::TcpListener::from_std(listener) {
|
|
413
505
|
Ok(listener) => serve(TcpOrUnix::Tcp(listener), server, token, stop_rx).await,
|
|
414
|
-
Err(e) =>
|
|
506
|
+
Err(e) => native_error(format!("control listener failed: {e}")),
|
|
415
507
|
},
|
|
416
508
|
ControlBind::Unix(listener, _) => match tokio::net::UnixListener::from_std(listener) {
|
|
417
509
|
Ok(listener) => serve(TcpOrUnix::Unix(listener), server, token, stop_rx).await,
|
|
418
|
-
Err(e) =>
|
|
510
|
+
Err(e) => native_error(format!("control listener failed: {e}")),
|
|
419
511
|
},
|
|
420
512
|
}
|
|
421
513
|
});
|
|
422
514
|
}
|
|
423
515
|
|
|
516
|
+
/// A failure inside the native layer itself, reported as the "native"
|
|
517
|
+
/// source: no Ruby ractor or thread spoke.
|
|
518
|
+
fn native_error(message: String) {
|
|
519
|
+
crate::log::emit(crate::log::Level::Error, "native", &message);
|
|
520
|
+
}
|
|
521
|
+
|
|
424
522
|
enum TcpOrUnix {
|
|
425
523
|
Tcp(tokio::net::TcpListener),
|
|
426
524
|
Unix(tokio::net::UnixListener),
|
|
@@ -440,7 +538,9 @@ async fn serve(
|
|
|
440
538
|
($accepted:expr) => {
|
|
441
539
|
match $accepted {
|
|
442
540
|
Ok((stream, _)) => {
|
|
443
|
-
let Ok(permit) = permits.clone().try_acquire_owned() else {
|
|
541
|
+
let Ok(permit) = permits.clone().try_acquire_owned() else {
|
|
542
|
+
continue;
|
|
543
|
+
};
|
|
444
544
|
spawn_connection(stream, permit, server.clone(), token.clone());
|
|
445
545
|
}
|
|
446
546
|
Err(_) => tokio::time::sleep(Duration::from_millis(100)).await,
|
|
@@ -496,8 +596,13 @@ fn handle(
|
|
|
496
596
|
.get(hyper::header::AUTHORIZATION)
|
|
497
597
|
.and_then(|v| v.to_str().ok());
|
|
498
598
|
let snapshot = StatsSnapshot::take(server);
|
|
499
|
-
let (status, content_type, body) =
|
|
500
|
-
|
|
599
|
+
let (status, content_type, body) = route(
|
|
600
|
+
req.method().as_str(),
|
|
601
|
+
req.uri().path(),
|
|
602
|
+
authorization,
|
|
603
|
+
token,
|
|
604
|
+
&snapshot,
|
|
605
|
+
);
|
|
501
606
|
let mut builder = hyper::Response::builder()
|
|
502
607
|
.status(status)
|
|
503
608
|
.header("content-type", content_type);
|
|
@@ -520,11 +625,28 @@ mod tests {
|
|
|
520
625
|
|
|
521
626
|
fn snapshot(state: u8) -> StatsSnapshot {
|
|
522
627
|
StatsSnapshot {
|
|
523
|
-
mode: "ractor".to_string(),
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
628
|
+
mode: "ractor".to_string(),
|
|
629
|
+
lanes: false,
|
|
630
|
+
workers: 8,
|
|
631
|
+
threads: 1,
|
|
632
|
+
batch: 1,
|
|
633
|
+
respawns: 2,
|
|
634
|
+
queued: 3,
|
|
635
|
+
in_flight: 4,
|
|
636
|
+
served: 100,
|
|
637
|
+
rejected: 5,
|
|
638
|
+
timeouts: 6,
|
|
639
|
+
lane_depths: None,
|
|
640
|
+
state,
|
|
641
|
+
worker_status: vec![],
|
|
642
|
+
quarantined_count: 0,
|
|
643
|
+
quarantine_replacements: 0,
|
|
644
|
+
queue_histogram: crate::registry::QueueHistogramSnapshot {
|
|
645
|
+
buckets: [0; crate::registry::QUEUE_BOUNDS_US.len()],
|
|
646
|
+
overflow: 0,
|
|
647
|
+
sum_us: 0,
|
|
648
|
+
count: 0,
|
|
649
|
+
},
|
|
528
650
|
}
|
|
529
651
|
}
|
|
530
652
|
|
|
@@ -532,10 +654,18 @@ mod tests {
|
|
|
532
654
|
fn stats_json_reports_every_field_and_the_state_name() {
|
|
533
655
|
let json = stats_json(&snapshot(crate::registry::STATE_READY));
|
|
534
656
|
for needle in [
|
|
535
|
-
r#""mode":"ractor""#,
|
|
536
|
-
r#""
|
|
537
|
-
r#""
|
|
538
|
-
r#""
|
|
657
|
+
r#""mode":"ractor""#,
|
|
658
|
+
r#""lanes":false"#,
|
|
659
|
+
r#""workers":8"#,
|
|
660
|
+
r#""threads":1"#,
|
|
661
|
+
r#""batch":1"#,
|
|
662
|
+
r#""respawns":2"#,
|
|
663
|
+
r#""queued":3"#,
|
|
664
|
+
r#""in_flight":4"#,
|
|
665
|
+
r#""served":100"#,
|
|
666
|
+
r#""rejected":5"#,
|
|
667
|
+
r#""timeouts":6"#,
|
|
668
|
+
r#""state":"ready""#,
|
|
539
669
|
r#""version":""#,
|
|
540
670
|
] {
|
|
541
671
|
assert!(json.contains(needle), "missing {needle} in {json}");
|
|
@@ -609,7 +739,10 @@ mod tests {
|
|
|
609
739
|
|
|
610
740
|
// The token guards stats and metrics; the probes stay open.
|
|
611
741
|
assert_eq!(route("GET", "/stats", None, Some("t"), &ready).0, 401);
|
|
612
|
-
assert_eq!(
|
|
742
|
+
assert_eq!(
|
|
743
|
+
route("GET", "/metrics", Some("Bearer t"), Some("t"), &ready).0,
|
|
744
|
+
200
|
|
745
|
+
);
|
|
613
746
|
assert_eq!(route("GET", "/ready", None, Some("t"), &ready).0, 200);
|
|
614
747
|
assert_eq!(route("GET", "/live", None, Some("t"), &ready).0, 200);
|
|
615
748
|
}
|
|
@@ -618,8 +751,20 @@ mod tests {
|
|
|
618
751
|
fn stats_json_emits_worker_status_array() {
|
|
619
752
|
let mut s = snapshot(crate::registry::STATE_READY);
|
|
620
753
|
s.worker_status = vec![
|
|
621
|
-
WorkerStat {
|
|
622
|
-
|
|
754
|
+
WorkerStat {
|
|
755
|
+
index: 0,
|
|
756
|
+
served: 10,
|
|
757
|
+
in_flight: 1,
|
|
758
|
+
busy_ms: 4,
|
|
759
|
+
quarantined: false,
|
|
760
|
+
},
|
|
761
|
+
WorkerStat {
|
|
762
|
+
index: 1,
|
|
763
|
+
served: 7,
|
|
764
|
+
in_flight: 0,
|
|
765
|
+
busy_ms: 0,
|
|
766
|
+
quarantined: false,
|
|
767
|
+
},
|
|
623
768
|
];
|
|
624
769
|
let json = stats_json(&s);
|
|
625
770
|
assert!(json.contains(r#""worker_status":[{"index":0,"served":10,"in_flight":1,"busy_ms":4,"quarantined":false},{"index":1,"served":7,"in_flight":0,"busy_ms":0,"quarantined":false}]"#), "got {json}");
|
|
@@ -635,8 +780,20 @@ mod tests {
|
|
|
635
780
|
fn metrics_text_emits_per_worker_series() {
|
|
636
781
|
let mut s = snapshot(crate::registry::STATE_READY);
|
|
637
782
|
s.worker_status = vec![
|
|
638
|
-
WorkerStat {
|
|
639
|
-
|
|
783
|
+
WorkerStat {
|
|
784
|
+
index: 0,
|
|
785
|
+
served: 10,
|
|
786
|
+
in_flight: 1,
|
|
787
|
+
busy_ms: 4,
|
|
788
|
+
quarantined: false,
|
|
789
|
+
},
|
|
790
|
+
WorkerStat {
|
|
791
|
+
index: 1,
|
|
792
|
+
served: 7,
|
|
793
|
+
in_flight: 0,
|
|
794
|
+
busy_ms: 0,
|
|
795
|
+
quarantined: false,
|
|
796
|
+
},
|
|
640
797
|
];
|
|
641
798
|
let text = metrics_text(&s);
|
|
642
799
|
assert!(text.contains("# TYPE kino_worker_requests_served_total counter"));
|
|
@@ -652,9 +809,15 @@ mod tests {
|
|
|
652
809
|
server.register_worker();
|
|
653
810
|
{
|
|
654
811
|
let slots = server.slots.read();
|
|
655
|
-
slots[0]
|
|
656
|
-
|
|
657
|
-
|
|
812
|
+
slots[0]
|
|
813
|
+
.in_flight
|
|
814
|
+
.store(1, std::sync::atomic::Ordering::Relaxed);
|
|
815
|
+
slots[0]
|
|
816
|
+
.last_started_ms
|
|
817
|
+
.store(0, std::sync::atomic::Ordering::Relaxed);
|
|
818
|
+
slots[0]
|
|
819
|
+
.quarantined
|
|
820
|
+
.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
658
821
|
}
|
|
659
822
|
let rows = collect_worker_status(&server);
|
|
660
823
|
assert!(rows[0].quarantined);
|
|
@@ -666,12 +829,30 @@ mod tests {
|
|
|
666
829
|
let mut s = snapshot(crate::registry::STATE_READY);
|
|
667
830
|
s.quarantined_count = 1;
|
|
668
831
|
s.worker_status = vec![
|
|
669
|
-
WorkerStat {
|
|
670
|
-
|
|
832
|
+
WorkerStat {
|
|
833
|
+
index: 0,
|
|
834
|
+
served: 3,
|
|
835
|
+
in_flight: 1,
|
|
836
|
+
busy_ms: 0,
|
|
837
|
+
quarantined: true,
|
|
838
|
+
},
|
|
839
|
+
WorkerStat {
|
|
840
|
+
index: 1,
|
|
841
|
+
served: 9,
|
|
842
|
+
in_flight: 1,
|
|
843
|
+
busy_ms: 5,
|
|
844
|
+
quarantined: false,
|
|
845
|
+
},
|
|
671
846
|
];
|
|
672
847
|
let json = stats_json(&s);
|
|
673
|
-
assert!(
|
|
674
|
-
|
|
848
|
+
assert!(
|
|
849
|
+
json.contains(r#""quarantined":1"#),
|
|
850
|
+
"top-level count: {json}"
|
|
851
|
+
);
|
|
852
|
+
assert!(
|
|
853
|
+
json.contains(r#"{"index":0,"served":3,"in_flight":1,"busy_ms":0,"quarantined":true}"#),
|
|
854
|
+
"{json}"
|
|
855
|
+
);
|
|
675
856
|
assert!(json.contains(r#""quarantined":false"#));
|
|
676
857
|
}
|
|
677
858
|
|
|
@@ -694,7 +875,10 @@ mod tests {
|
|
|
694
875
|
buckets[0] = 3; // <= 0.0005s
|
|
695
876
|
buckets[2] = 1; // <= 0.0025s
|
|
696
877
|
s.queue_histogram = crate::registry::QueueHistogramSnapshot {
|
|
697
|
-
buckets,
|
|
878
|
+
buckets,
|
|
879
|
+
overflow: 1,
|
|
880
|
+
sum_us: 3 * 100 + 2_000 + 20_000_000,
|
|
881
|
+
count: 5,
|
|
698
882
|
};
|
|
699
883
|
let text = metrics_text(&s);
|
|
700
884
|
assert!(text.contains("# TYPE kino_request_queue_seconds histogram"));
|
|
@@ -710,9 +894,15 @@ mod tests {
|
|
|
710
894
|
fn stats_json_reports_queue_time() {
|
|
711
895
|
let mut s = snapshot(crate::registry::STATE_READY);
|
|
712
896
|
s.queue_histogram = crate::registry::QueueHistogramSnapshot {
|
|
713
|
-
buckets: [0; crate::registry::QUEUE_BOUNDS_US.len()],
|
|
897
|
+
buckets: [0; crate::registry::QUEUE_BOUNDS_US.len()],
|
|
898
|
+
overflow: 0,
|
|
899
|
+
sum_us: 1_500_000,
|
|
900
|
+
count: 2,
|
|
714
901
|
};
|
|
715
902
|
let json = stats_json(&s);
|
|
716
|
-
assert!(
|
|
903
|
+
assert!(
|
|
904
|
+
json.contains(r#""queue_time":{"count":2,"sum_seconds":1.5}"#),
|
|
905
|
+
"{json}"
|
|
906
|
+
);
|
|
717
907
|
}
|
|
718
908
|
}
|
|
@@ -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/env_strings.rs
CHANGED
|
@@ -302,7 +302,11 @@ mod tests {
|
|
|
302
302
|
fn common_headers_are_lowercase_and_unique() {
|
|
303
303
|
let mut seen = std::collections::HashSet::new();
|
|
304
304
|
for header in COMMON_HEADERS {
|
|
305
|
-
assert_eq!(
|
|
305
|
+
assert_eq!(
|
|
306
|
+
*header,
|
|
307
|
+
header.to_ascii_lowercase(),
|
|
308
|
+
"{header} must be lowercase"
|
|
309
|
+
);
|
|
306
310
|
assert!(seen.insert(*header), "{header} listed twice");
|
|
307
311
|
}
|
|
308
312
|
}
|
|
@@ -311,7 +315,11 @@ mod tests {
|
|
|
311
315
|
fn common_methods_are_uppercase_and_unique() {
|
|
312
316
|
let mut seen = std::collections::HashSet::new();
|
|
313
317
|
for method in COMMON_METHODS {
|
|
314
|
-
assert_eq!(
|
|
318
|
+
assert_eq!(
|
|
319
|
+
*method,
|
|
320
|
+
method.to_ascii_uppercase(),
|
|
321
|
+
"{method} must be uppercase"
|
|
322
|
+
);
|
|
315
323
|
assert!(seen.insert(*method), "{method} listed twice");
|
|
316
324
|
}
|
|
317
325
|
}
|