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.
@@ -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.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))
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 crate::server::log_error(format!("control runtime failed: {e}")),
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) => crate::server::log_error(format!("control listener failed: {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) => crate::server::log_error(format!("control listener failed: {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("log_error", function!(server::log_error, 1))?;
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
+ }
@@ -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
+ }
@@ -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
- server
123
- .queue_histogram
124
- .record(ctx.enqueued_at.elapsed().as_micros() as u64);
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.
@@ -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),
@@ -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