kino 0.4.0 → 0.6.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 +56 -0
- data/Cargo.lock +115 -24
- data/README.md +113 -41
- data/doc/architecture.md +61 -7
- data/doc/benchmarks.md +120 -26
- data/ext/kino/Cargo.toml +8 -3
- data/ext/kino/src/control.rs +267 -60
- data/ext/kino/src/env_strings.rs +172 -12
- data/ext/kino/src/io_shards.rs +437 -0
- data/ext/kino/src/lib.rs +12 -7
- data/ext/kino/src/listen.rs +4 -1
- data/ext/kino/src/log.rs +10 -2
- data/ext/kino/src/queue.rs +61 -44
- data/ext/kino/src/registry.rs +103 -10
- data/ext/kino/src/request.rs +150 -69
- data/ext/kino/src/response.rs +40 -9
- data/ext/kino/src/server.rs +759 -97
- data/ext/kino/src/tls.rs +129 -12
- data/lib/kino/configuration.rb +16 -0
- data/lib/kino/server.rb +10 -0
- data/lib/kino/templates/kino.rb.tt +13 -2
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +14 -11
- data/sig/kino.rbs +3 -0
- metadata +16 -1
data/ext/kino/src/lib.rs
CHANGED
|
@@ -9,6 +9,7 @@ mod control;
|
|
|
9
9
|
mod cpus;
|
|
10
10
|
mod env_strings;
|
|
11
11
|
mod gvl;
|
|
12
|
+
mod io_shards;
|
|
12
13
|
mod listen;
|
|
13
14
|
mod log;
|
|
14
15
|
mod logsink;
|
|
@@ -40,8 +41,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
40
41
|
let native = module.define_module("Native")?;
|
|
41
42
|
native.define_singleton_method("server_start", function!(server::server_start, 1))?;
|
|
42
43
|
native.define_singleton_method("register_worker", function!(server::register_worker, 1))?;
|
|
43
|
-
native.define_singleton_method("
|
|
44
|
-
native.define_singleton_method("take_batch", function!(queue::take_batch, 3))?;
|
|
44
|
+
native.define_singleton_method("worker", function!(queue::worker, 2))?;
|
|
45
45
|
native.define_singleton_method("stop_accepting", function!(server::stop_accepting, 1))?;
|
|
46
46
|
native.define_singleton_method("close_queue", function!(server::close_queue, 1))?;
|
|
47
47
|
native.define_singleton_method("queue_stats", function!(server::queue_stats, 1))?;
|
|
@@ -83,11 +83,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
83
83
|
native.define_class("PinKeeper", ruby.class_object())?;
|
|
84
84
|
native.define_singleton_method("pin_keeper", function!(server::pin_keeper, 1))?;
|
|
85
85
|
|
|
86
|
+
let worker = native.define_class("Worker", ruby.class_object())?;
|
|
87
|
+
worker.define_method("take_one", method!(queue::Worker::take_one, 0))?;
|
|
88
|
+
worker.define_method("take_batch", method!(queue::Worker::take_batch, 1))?;
|
|
89
|
+
|
|
86
90
|
let request = native.define_class("Request", ruby.class_object())?;
|
|
87
|
-
request.define_method("respond_and_take", method!(queue::respond_and_take,
|
|
91
|
+
request.define_method("respond_and_take", method!(queue::respond_and_take, 5))?;
|
|
88
92
|
request.define_method(
|
|
89
93
|
"respond_and_take_one",
|
|
90
|
-
method!(queue::respond_and_take_one,
|
|
94
|
+
method!(queue::respond_and_take_one, 4),
|
|
91
95
|
)?;
|
|
92
96
|
request.define_method("read_body", method!(Request::read_body, 1))?;
|
|
93
97
|
request.define_method("send_simple", method!(crate::request::respond_simple, 3))?;
|
|
@@ -97,10 +101,11 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
97
101
|
request.define_method("abort", method!(Request::abort, 0))?;
|
|
98
102
|
request.define_method("timing", method!(Request::set_timing, 2))?;
|
|
99
103
|
|
|
100
|
-
// Force-resolve the TypedData class
|
|
101
|
-
// resolves
|
|
102
|
-
// worker ractors is the failure mode we must rule out.
|
|
104
|
+
// Force-resolve the TypedData class caches on the main ractor: magnus
|
|
105
|
+
// resolves them lazily on first wrap, and a racy first resolution from
|
|
106
|
+
// two worker ractors is the failure mode we must rule out.
|
|
103
107
|
let _ = <Request as magnus::TypedData>::class(ruby);
|
|
108
|
+
let _ = <queue::Worker as magnus::TypedData>::class(ruby);
|
|
104
109
|
|
|
105
110
|
// Frozen env key/value cache: built once here (main ractor, GVL held),
|
|
106
111
|
// shared by every worker ractor afterwards.
|
data/ext/kino/src/listen.rs
CHANGED
|
@@ -86,7 +86,10 @@ mod tests {
|
|
|
86
86
|
|
|
87
87
|
#[test]
|
|
88
88
|
fn unix_path_recognises_only_the_unix_scheme() {
|
|
89
|
-
assert_eq!(
|
|
89
|
+
assert_eq!(
|
|
90
|
+
unix_path("unix:///run/kino.sock").unwrap().to_str(),
|
|
91
|
+
Some("/run/kino.sock")
|
|
92
|
+
);
|
|
90
93
|
assert!(unix_path("127.0.0.1").is_none());
|
|
91
94
|
assert!(unix_path("unix.example.com").is_none());
|
|
92
95
|
}
|
data/ext/kino/src/log.rs
CHANGED
|
@@ -99,7 +99,10 @@ mod tests {
|
|
|
99
99
|
#[test]
|
|
100
100
|
fn label_is_a_syslog_tag_plus_the_source() {
|
|
101
101
|
assert_eq!(label(4213, "main"), "kino[4213] main:");
|
|
102
|
-
assert_eq!(
|
|
102
|
+
assert_eq!(
|
|
103
|
+
label(4213, "worker-3/thread-2"),
|
|
104
|
+
"kino[4213] worker-3/thread-2:"
|
|
105
|
+
);
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
#[test]
|
|
@@ -129,7 +132,12 @@ mod tests {
|
|
|
129
132
|
#[test]
|
|
130
133
|
fn a_report_is_labelled_on_its_first_line_only() {
|
|
131
134
|
assert_eq!(
|
|
132
|
-
format_line(
|
|
135
|
+
format_line(
|
|
136
|
+
Level::Error,
|
|
137
|
+
"kino[1] main:",
|
|
138
|
+
"500 GET / · X: y\n a.rb:1",
|
|
139
|
+
false
|
|
140
|
+
),
|
|
133
141
|
"kino[1] main: 500 GET / · X: y\n a.rb:1"
|
|
134
142
|
);
|
|
135
143
|
}
|
data/ext/kino/src/queue.rs
CHANGED
|
@@ -117,7 +117,8 @@ fn admit(
|
|
|
117
117
|
) -> Result<RHash, Error> {
|
|
118
118
|
server.served.fetch_add(1, Ordering::Relaxed);
|
|
119
119
|
slot.served.fetch_add(1, Ordering::Relaxed);
|
|
120
|
-
slot.last_started_ms
|
|
120
|
+
slot.last_started_ms
|
|
121
|
+
.store(crate::mono::mono_ms(), Ordering::Relaxed);
|
|
121
122
|
slot.in_flight.fetch_add(1, Ordering::Relaxed);
|
|
122
123
|
// One clock read serves the histogram and, for the access log, the
|
|
123
124
|
// request's queue wait and the start of its time in Ruby.
|
|
@@ -137,54 +138,73 @@ fn admit(
|
|
|
137
138
|
Ok(env)
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
/// Per-worker native handle: the registry lookup and slot resolution are
|
|
142
|
+
/// paid once at worker boot instead of on every take. Created inside the
|
|
143
|
+
/// worker thread/ractor that uses it (like Request handles), so ownership
|
|
144
|
+
/// is correct by construction. Holds the server weakly: after teardown the
|
|
145
|
+
/// upgrade fails, which is the same clean shutdown signal the per-take
|
|
146
|
+
/// registry lookup used to give.
|
|
147
|
+
#[magnus::wrap(class = "Kino::Native::Worker", free_immediately)]
|
|
148
|
+
pub struct Worker {
|
|
149
|
+
server: std::sync::Weak<ServerInner>,
|
|
150
|
+
slot: Arc<WorkerSlot>,
|
|
151
|
+
}
|
|
141
152
|
|
|
142
|
-
|
|
153
|
+
/// Resolve a (server, worker) pair into a Worker handle; nil when the
|
|
154
|
+
/// server is already gone (the caller treats that as shutdown).
|
|
155
|
+
pub fn worker(ruby: &Ruby, server_id: u64, worker_id: usize) -> Result<Option<Worker>, Error> {
|
|
143
156
|
let Some(server) = registry::try_get(server_id) else {
|
|
144
|
-
return Ok(None);
|
|
157
|
+
return Ok(None);
|
|
145
158
|
};
|
|
146
159
|
let slot = server.slot(ruby, worker_id)?;
|
|
160
|
+
Ok(Some(Worker {
|
|
161
|
+
server: Arc::downgrade(&server),
|
|
162
|
+
slot,
|
|
163
|
+
}))
|
|
164
|
+
}
|
|
147
165
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
166
|
+
impl Worker {
|
|
167
|
+
fn checkout(&self) -> Result<Option<(Arc<ServerInner>, BoxedCtx)>, Error> {
|
|
168
|
+
let Some(server) = self.server.upgrade() else {
|
|
169
|
+
return Ok(None); // server torn down → clean shutdown signal
|
|
170
|
+
};
|
|
152
171
|
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
// The previous batch is fully answered once the worker comes back.
|
|
173
|
+
self.slot.current.lock().clear();
|
|
174
|
+
self.slot.in_flight.store(0, Ordering::Relaxed);
|
|
175
|
+
self.slot.interrupted.store(false, Ordering::SeqCst);
|
|
155
176
|
|
|
156
|
-
|
|
157
|
-
/// "kino.request") or nil on shutdown. The batch-of-one hot path: no
|
|
158
|
-
/// arrays allocated at all.
|
|
159
|
-
pub fn take_one(ruby: &Ruby, server_id: u64, worker_id: usize) -> Result<Option<RHash>, Error> {
|
|
160
|
-
match checkout(ruby, server_id, worker_id)? {
|
|
161
|
-
Some((server, slot, ctx)) => Ok(Some(admit(ruby, &server, &slot, ctx)?)),
|
|
162
|
-
None => Ok(None),
|
|
177
|
+
Ok(block_take(&server, &self.slot)?.map(|ctx| (server, ctx)))
|
|
163
178
|
}
|
|
164
|
-
}
|
|
165
179
|
|
|
166
|
-
/// Take
|
|
167
|
-
///
|
|
168
|
-
///
|
|
169
|
-
pub fn
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
let Some((server, slot, first)) = checkout(ruby, server_id, worker_id)? else {
|
|
176
|
-
return Ok(None);
|
|
177
|
-
};
|
|
180
|
+
/// Take one request; returns its env Hash (request handle inside under
|
|
181
|
+
/// "kino.request") or nil on shutdown. The batch-of-one hot path: no
|
|
182
|
+
/// arrays allocated at all.
|
|
183
|
+
pub fn take_one(ruby: &Ruby, rb_self: &Worker) -> Result<Option<RHash>, Error> {
|
|
184
|
+
match rb_self.checkout()? {
|
|
185
|
+
Some((server, ctx)) => Ok(Some(admit(ruby, &server, &rb_self.slot, ctx)?)),
|
|
186
|
+
None => Ok(None),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
178
189
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
/// Take up to `max` requests: block for the first, drain the rest
|
|
191
|
+
/// non-blocking (they only batch when the queue is already deep).
|
|
192
|
+
/// Returns nil on shutdown; otherwise an Array of env Hashes.
|
|
193
|
+
pub fn take_batch(ruby: &Ruby, rb_self: &Worker, max: usize) -> Result<Option<RArray>, Error> {
|
|
194
|
+
let Some((server, first)) = rb_self.checkout()? else {
|
|
195
|
+
return Ok(None);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
let batch = ruby.ary_new_capa(max.max(1));
|
|
199
|
+
batch.push(admit(ruby, &server, &rb_self.slot, first)?)?;
|
|
200
|
+
for _ in 1..max {
|
|
201
|
+
match server.req_rx.try_recv() {
|
|
202
|
+
Ok(ctx) => batch.push(admit(ruby, &server, &rb_self.slot, ctx)?)?,
|
|
203
|
+
Err(_) => break,
|
|
204
|
+
}
|
|
185
205
|
}
|
|
206
|
+
Ok(Some(batch))
|
|
186
207
|
}
|
|
187
|
-
Ok(Some(batch))
|
|
188
208
|
}
|
|
189
209
|
|
|
190
210
|
/// The fused hot path: answer `request` (complete response in one shot)
|
|
@@ -193,28 +213,25 @@ pub fn take_batch(
|
|
|
193
213
|
pub fn respond_and_take_one(
|
|
194
214
|
ruby: &Ruby,
|
|
195
215
|
request: &Request,
|
|
196
|
-
|
|
197
|
-
worker_id: usize,
|
|
216
|
+
worker: magnus::typed_data::Obj<Worker>,
|
|
198
217
|
status: u16,
|
|
199
218
|
headers: RHash,
|
|
200
219
|
body: RString,
|
|
201
220
|
) -> Result<Option<RHash>, Error> {
|
|
202
221
|
crate::request::respond_simple(ruby, request, status, headers, body)?;
|
|
203
|
-
take_one(ruby,
|
|
222
|
+
Worker::take_one(ruby, &worker)
|
|
204
223
|
}
|
|
205
224
|
|
|
206
225
|
/// Batch variant of the fused call.
|
|
207
|
-
#[allow(clippy::too_many_arguments)]
|
|
208
226
|
pub fn respond_and_take(
|
|
209
227
|
ruby: &Ruby,
|
|
210
228
|
request: &Request,
|
|
211
|
-
|
|
212
|
-
worker_id: usize,
|
|
229
|
+
worker: magnus::typed_data::Obj<Worker>,
|
|
213
230
|
max: usize,
|
|
214
231
|
status: u16,
|
|
215
232
|
headers: RHash,
|
|
216
233
|
body: RString,
|
|
217
234
|
) -> Result<Option<RArray>, Error> {
|
|
218
235
|
crate::request::respond_simple(ruby, request, status, headers, body)?;
|
|
219
|
-
take_batch(ruby,
|
|
236
|
+
Worker::take_batch(ruby, &worker, max)
|
|
220
237
|
}
|
data/ext/kino/src/registry.rs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
|
7
7
|
use std::sync::{Arc, OnceLock, Weak};
|
|
8
|
+
use std::time::{Duration, Instant};
|
|
8
9
|
|
|
9
10
|
use parking_lot::{Mutex, RwLock};
|
|
10
11
|
|
|
@@ -32,6 +33,51 @@ pub type BoxedCtx = Box<RequestCtx>;
|
|
|
32
33
|
/// Probed on every take; keys are our own ids, so ahash over SipHash.
|
|
33
34
|
type HashMap<K, V> = std::collections::HashMap<K, V, ahash::RandomState>;
|
|
34
35
|
|
|
36
|
+
/// What runs the server's I/O, taken out at shutdown. The default
|
|
37
|
+
/// multi-thread runtime and sharded I/O carry different teardown state,
|
|
38
|
+
/// so the shape stays explicit instead of parallel optional fields.
|
|
39
|
+
#[derive(Default)]
|
|
40
|
+
pub enum RuntimeHandle {
|
|
41
|
+
/// Not started yet, or already shut down.
|
|
42
|
+
#[default]
|
|
43
|
+
None,
|
|
44
|
+
MultiThread(tokio::runtime::Runtime),
|
|
45
|
+
/// The shards' final-teardown signal and every I/O thread (shards plus
|
|
46
|
+
/// the acceptor).
|
|
47
|
+
Shards {
|
|
48
|
+
shutdown_tx: tokio::sync::watch::Sender<bool>,
|
|
49
|
+
threads: Vec<std::thread::JoinHandle<()>>,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl RuntimeHandle {
|
|
54
|
+
/// Stop the I/O side, giving in-flight work `timeout` to finish. A
|
|
55
|
+
/// thread that misses the deadline is abandoned rather than joined:
|
|
56
|
+
/// a wedged shard must not hang `Server#shutdown`, which promises to
|
|
57
|
+
/// return by its deadline.
|
|
58
|
+
pub fn shutdown(self, timeout: Duration) {
|
|
59
|
+
match self {
|
|
60
|
+
RuntimeHandle::None => {}
|
|
61
|
+
RuntimeHandle::MultiThread(runtime) => runtime.shutdown_timeout(timeout),
|
|
62
|
+
RuntimeHandle::Shards {
|
|
63
|
+
shutdown_tx,
|
|
64
|
+
threads,
|
|
65
|
+
} => {
|
|
66
|
+
let _ = shutdown_tx.send(true);
|
|
67
|
+
let deadline = Instant::now() + timeout;
|
|
68
|
+
for thread in threads {
|
|
69
|
+
while !thread.is_finished() && Instant::now() < deadline {
|
|
70
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
71
|
+
}
|
|
72
|
+
if thread.is_finished() {
|
|
73
|
+
let _ = thread.join();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
35
81
|
/// One per `Kino::Server`. Owns the tokio runtime, the request queue and the
|
|
36
82
|
/// worker slots.
|
|
37
83
|
pub struct ServerInner {
|
|
@@ -42,9 +88,9 @@ pub struct ServerInner {
|
|
|
42
88
|
pub req_rx: flume::Receiver<BoxedCtx>,
|
|
43
89
|
/// Signals the accept loop to stop. Watch channel: `true` = draining.
|
|
44
90
|
pub shutdown_tx: tokio::sync::watch::Sender<bool>,
|
|
45
|
-
/// Runtime is kept so we can shut it down explicitly;
|
|
46
|
-
///
|
|
47
|
-
pub runtime: Mutex<
|
|
91
|
+
/// Runtime is kept so we can shut it down explicitly; `shutdown_runtime`
|
|
92
|
+
/// takes ownership out of the Arc.
|
|
93
|
+
pub runtime: Mutex<RuntimeHandle>,
|
|
48
94
|
pub slots: RwLock<Vec<Arc<WorkerSlot>>>,
|
|
49
95
|
pub in_flight: AtomicUsize,
|
|
50
96
|
/// Requests handed to Ruby workers (admitted), and requests rejected
|
|
@@ -69,6 +115,9 @@ pub struct ServerInner {
|
|
|
69
115
|
pub quarantine_replacements: AtomicU64,
|
|
70
116
|
pub topology: Topology,
|
|
71
117
|
pub https: bool,
|
|
118
|
+
/// HTTP/2 serving (ALPN over TLS, prior-knowledge h2c on plaintext);
|
|
119
|
+
/// false pins every connection to HTTP/1.
|
|
120
|
+
pub http2: bool,
|
|
72
121
|
/// The socket file of a `unix://` bind, removed at shutdown.
|
|
73
122
|
pub unix_path: Option<std::path::PathBuf>,
|
|
74
123
|
/// Native access log sink (None unless log_requests is on).
|
|
@@ -118,8 +167,8 @@ pub const LANE_DEPTH: usize = 4;
|
|
|
118
167
|
/// Fixed queue-wait bucket boundaries in microseconds (0.5ms .. 10s),
|
|
119
168
|
/// ascending. Emitted in seconds. Not a knob (YAGNI).
|
|
120
169
|
pub const QUEUE_BOUNDS_US: [u64; 14] = [
|
|
121
|
-
500, 1_000, 2_500, 5_000, 10_000, 25_000, 50_000, 100_000,
|
|
122
|
-
|
|
170
|
+
500, 1_000, 2_500, 5_000, 10_000, 25_000, 50_000, 100_000, 250_000, 500_000, 1_000_000,
|
|
171
|
+
2_500_000, 5_000_000, 10_000_000,
|
|
123
172
|
];
|
|
124
173
|
|
|
125
174
|
/// Queue-wait histogram: per-bucket counts plus an overflow (the implicit
|
|
@@ -289,7 +338,7 @@ pub fn test_server(lanes: bool, queue_depth: usize) -> Arc<ServerInner> {
|
|
|
289
338
|
req_tx: Mutex::new(Some(req_tx)),
|
|
290
339
|
req_rx,
|
|
291
340
|
shutdown_tx,
|
|
292
|
-
runtime: Mutex::new(None),
|
|
341
|
+
runtime: Mutex::new(RuntimeHandle::None),
|
|
293
342
|
slots: RwLock::new(Vec::new()),
|
|
294
343
|
in_flight: AtomicUsize::new(0),
|
|
295
344
|
served: AtomicU64::new(0),
|
|
@@ -301,8 +350,14 @@ pub fn test_server(lanes: bool, queue_depth: usize) -> Arc<ServerInner> {
|
|
|
301
350
|
state: std::sync::atomic::AtomicU8::new(STATE_BOOTING),
|
|
302
351
|
respawns: AtomicU64::new(0),
|
|
303
352
|
quarantine_replacements: AtomicU64::new(0),
|
|
304
|
-
topology: Topology {
|
|
353
|
+
topology: Topology {
|
|
354
|
+
mode: "threaded".to_string(),
|
|
355
|
+
workers: 0,
|
|
356
|
+
threads: 0,
|
|
357
|
+
batch: 1,
|
|
358
|
+
},
|
|
305
359
|
https: false,
|
|
360
|
+
http2: true,
|
|
306
361
|
unix_path: None,
|
|
307
362
|
access_log: None,
|
|
308
363
|
lanes,
|
|
@@ -317,6 +372,44 @@ mod tests {
|
|
|
317
372
|
use super::*;
|
|
318
373
|
use crate::request::test_ctx;
|
|
319
374
|
|
|
375
|
+
#[test]
|
|
376
|
+
fn shards_shutdown_joins_threads_that_observe_the_signal() {
|
|
377
|
+
let (shutdown_tx, rx) = tokio::sync::watch::channel(false);
|
|
378
|
+
let thread = std::thread::spawn(move || {
|
|
379
|
+
while !*rx.borrow() {
|
|
380
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
let start = Instant::now();
|
|
385
|
+
RuntimeHandle::Shards {
|
|
386
|
+
shutdown_tx,
|
|
387
|
+
threads: vec![thread],
|
|
388
|
+
}
|
|
389
|
+
.shutdown(Duration::from_secs(5));
|
|
390
|
+
|
|
391
|
+
// The thread exits on the signal, so the join comes nowhere near
|
|
392
|
+
// the deadline.
|
|
393
|
+
assert!(start.elapsed() < Duration::from_secs(1));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
fn shards_shutdown_abandons_a_wedged_thread_at_the_deadline() {
|
|
398
|
+
let (shutdown_tx, _rx) = tokio::sync::watch::channel(false);
|
|
399
|
+
let wedged = std::thread::spawn(|| std::thread::sleep(Duration::from_secs(30)));
|
|
400
|
+
|
|
401
|
+
let start = Instant::now();
|
|
402
|
+
RuntimeHandle::Shards {
|
|
403
|
+
shutdown_tx,
|
|
404
|
+
threads: vec![wedged],
|
|
405
|
+
}
|
|
406
|
+
.shutdown(Duration::from_millis(50));
|
|
407
|
+
|
|
408
|
+
let elapsed = start.elapsed();
|
|
409
|
+
assert!(elapsed >= Duration::from_millis(50));
|
|
410
|
+
assert!(elapsed < Duration::from_secs(5));
|
|
411
|
+
}
|
|
412
|
+
|
|
320
413
|
#[test]
|
|
321
414
|
fn worker_registration_hands_out_sequential_slot_ids() {
|
|
322
415
|
let server = test_server(false, 4);
|
|
@@ -420,9 +513,9 @@ mod tests {
|
|
|
420
513
|
#[test]
|
|
421
514
|
fn queue_histogram_buckets_by_wait() {
|
|
422
515
|
let h = QueueHistogram::new();
|
|
423
|
-
h.record(400);
|
|
424
|
-
h.record(500);
|
|
425
|
-
h.record(600);
|
|
516
|
+
h.record(400); // <= 500 -> bucket 0
|
|
517
|
+
h.record(500); // == 500 -> bucket 0 (inclusive)
|
|
518
|
+
h.record(600); // (500, 1000] -> bucket 1
|
|
426
519
|
h.record(20_000_000); // > last bound -> overflow
|
|
427
520
|
let s = h.snapshot();
|
|
428
521
|
assert_eq!(s.buckets[0], 2);
|