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.
@@ -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.iter().map(|d| d.to_string()).collect::<Vec<_>>().join(",");
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, s.state_name(),
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!(out, "# HELP {name} {help}\n# TYPE {name} {kind}\n{name} {value}")
160
- .expect("writing to a String cannot fail");
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}").expect("writing to a String cannot fail");
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}").expect("writing to a String cannot fail");
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(&mut out, "kino_requests_served_total", "counter", "Requests handed to Ruby workers.", s.served);
185
- metric(&mut out, "kino_requests_rejected_total", "counter", "Requests rejected with a 503.", s.rejected);
186
- metric(&mut out, "kino_request_timeouts_total", "counter", "Responses past the request timeout (client got a 504).", s.timeouts);
187
- metric(&mut out, "kino_worker_respawns_total", "counter", "Crashed workers respawned by the supervisor.", s.respawns);
188
- metric(&mut out, "kino_queue_depth", "gauge", "Requests waiting for a worker.", s.queued);
189
- metric(&mut out, "kino_requests_in_flight", "gauge", "Requests currently inside Ruby workers.", s.in_flight);
190
- metric(&mut out, "kino_workers", "gauge", "Configured worker count.", s.workers);
191
- metric(&mut out, "kino_threads_per_worker", "gauge", "Configured threads per worker.", s.threads);
192
- metric(&mut out, "kino_ready", "gauge", "1 when serving, 0 while booting or draining.",
193
- if s.state == STATE_READY { "1" } else { "0" });
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(&mut out, "kino_lane_depth", "gauge", "Queued requests in each worker lane.", "lane",
196
- depths.iter().enumerate().map(|(lane, depth)| (lane, *depth)));
197
- }
198
- series(&mut out, "kino_worker_requests_served_total", "counter", "Requests handed to each dispatch slot.", "worker",
199
- s.worker_status.iter().map(|w| (w.index, w.served)));
200
- series(&mut out, "kino_worker_in_flight", "gauge", "Requests executing in each dispatch slot.", "worker",
201
- s.worker_status.iter().map(|w| (w.index, w.in_flight)));
202
- series(&mut out, "kino_worker_busy_ms", "gauge", "Age in ms of the current in-flight request per slot (0 when idle).", "worker",
203
- s.worker_status.iter().map(|w| (w.index, w.busy_ms)));
204
- metric(&mut out, "kino_quarantined_workers", "gauge", "Dispatch slots abandoned as wedged.", s.quarantined_count);
205
- metric(&mut out, "kino_quarantine_replacements_total", "counter", "Replacement workers spawned after a wedge.", s.quarantine_replacements);
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!(out, "kino_request_queue_seconds_bucket{{le=\"{le}\"}} {cumulative}")
213
- .expect("writing to a String cannot fail");
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!(out, "kino_request_queue_seconds_bucket{{le=\"+Inf\"}} {total}")
217
- .expect("writing to a String cannot fail");
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.strip_prefix("unix://") {
304
- let path = std::path::PathBuf::from(path);
305
- // A path that already exists is either a live listener (refuse: do
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 { stop_tx, join, unix_path },
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().enable_all().build() {
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 crate::server::log_error(format!("control runtime failed: {e}")),
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) => crate::server::log_error(format!("control listener failed: {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) => crate::server::log_error(format!("control listener failed: {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 { continue };
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
- route(req.method().as_str(), req.uri().path(), authorization, token, &snapshot);
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(), lanes: false, workers: 8, threads: 1,
524
- batch: 1, respawns: 2, queued: 3, in_flight: 4, served: 100,
525
- rejected: 5, timeouts: 6, lane_depths: None, state, worker_status: vec![],
526
- quarantined_count: 0, quarantine_replacements: 0,
527
- queue_histogram: crate::registry::QueueHistogramSnapshot { buckets: [0; crate::registry::QUEUE_BOUNDS_US.len()], overflow: 0, sum_us: 0, count: 0 },
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""#, r#""lanes":false"#, r#""workers":8"#,
536
- r#""threads":1"#, r#""batch":1"#, r#""respawns":2"#,
537
- r#""queued":3"#, r#""in_flight":4"#, r#""served":100"#,
538
- r#""rejected":5"#, r#""timeouts":6"#, r#""state":"ready""#,
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!(route("GET", "/metrics", Some("Bearer t"), Some("t"), &ready).0, 200);
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 { index: 0, served: 10, in_flight: 1, busy_ms: 4, quarantined: false },
622
- WorkerStat { index: 1, served: 7, in_flight: 0, busy_ms: 0, quarantined: false },
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 { index: 0, served: 10, in_flight: 1, busy_ms: 4, quarantined: false },
639
- WorkerStat { index: 1, served: 7, in_flight: 0, busy_ms: 0, quarantined: false },
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].in_flight.store(1, std::sync::atomic::Ordering::Relaxed);
656
- slots[0].last_started_ms.store(0, std::sync::atomic::Ordering::Relaxed);
657
- slots[0].quarantined.store(true, std::sync::atomic::Ordering::Relaxed);
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 { index: 0, served: 3, in_flight: 1, busy_ms: 0, quarantined: true },
670
- WorkerStat { index: 1, served: 9, in_flight: 1, busy_ms: 5, quarantined: false },
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!(json.contains(r#""quarantined":1"#), "top-level count: {json}");
674
- assert!(json.contains(r#"{"index":0,"served":3,"in_flight":1,"busy_ms":0,"quarantined":true}"#), "{json}");
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, overflow: 1, sum_us: 3 * 100 + 2_000 + 20_000_000, count: 5,
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()], overflow: 0, sum_us: 1_500_000, count: 2,
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!(json.contains(r#""queue_time":{"count":2,"sum_seconds":1.5}"#), "{json}");
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
+ }
@@ -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!(*header, header.to_ascii_lowercase(), "{header} must be lowercase");
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!(*method, method.to_ascii_uppercase(), "{method} must be uppercase");
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
  }