kino 0.4.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 +10 -0
- data/Cargo.lock +24 -24
- data/README.md +15 -0
- data/doc/architecture.md +7 -4
- data/ext/kino/Cargo.toml +1 -1
- data/ext/kino/src/control.rs +267 -60
- data/ext/kino/src/env_strings.rs +10 -2
- data/ext/kino/src/io_shards.rs +437 -0
- data/ext/kino/src/lib.rs +1 -0
- data/ext/kino/src/listen.rs +4 -1
- data/ext/kino/src/log.rs +10 -2
- data/ext/kino/src/queue.rs +2 -1
- data/ext/kino/src/registry.rs +99 -10
- data/ext/kino/src/response.rs +1 -4
- data/ext/kino/src/server.rs +112 -66
- data/ext/kino/src/tls.rs +11 -4
- data/lib/kino/configuration.rb +10 -0
- data/lib/kino/server.rb +8 -0
- data/lib/kino/templates/kino.rb.tt +8 -2
- data/lib/kino/version.rb +1 -1
- data/sig/kino.rbs +2 -0
- metadata +2 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
//! Current-thread Tokio runtimes for HTTP I/O.
|
|
2
|
+
//!
|
|
3
|
+
//! One accept thread owns the listener and assigns accepted connections to
|
|
4
|
+
//! the least-loaded shard. Each shard then owns that connection for its
|
|
5
|
+
//! lifetime, avoiding the shared Tokio worker pool on hot HTTP paths.
|
|
6
|
+
|
|
7
|
+
use std::net::SocketAddr;
|
|
8
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
use std::thread::JoinHandle;
|
|
11
|
+
|
|
12
|
+
use crate::listen::Listener;
|
|
13
|
+
use crate::log::{self, Level};
|
|
14
|
+
use crate::registry::{ServerInner, STATE_DRAINING};
|
|
15
|
+
use crate::server::{serve_conn, AsyncListener, Conn};
|
|
16
|
+
|
|
17
|
+
/// A connection in transit from the acceptor to its shard. Tokio streams
|
|
18
|
+
/// are bound to the runtime that registered them, so the handoff carries
|
|
19
|
+
/// the std stream and the shard re-registers it on arrival.
|
|
20
|
+
enum StdConn {
|
|
21
|
+
Tcp(std::net::TcpStream),
|
|
22
|
+
Unix(std::os::unix::net::UnixStream),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl StdConn {
|
|
26
|
+
/// Register with the calling (shard) runtime.
|
|
27
|
+
fn into_tokio(self) -> std::io::Result<Conn> {
|
|
28
|
+
Ok(match self {
|
|
29
|
+
StdConn::Tcp(stream) => Conn::Tcp(tokio::net::TcpStream::from_std(stream)?),
|
|
30
|
+
StdConn::Unix(stream) => Conn::Unix(tokio::net::UnixStream::from_std(stream)?),
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Detach an accepted stream from the acceptor's runtime for the handoff.
|
|
36
|
+
fn into_std(conn: Conn) -> std::io::Result<StdConn> {
|
|
37
|
+
Ok(match conn {
|
|
38
|
+
Conn::Tcp(stream) => StdConn::Tcp(stream.into_std()?),
|
|
39
|
+
Conn::Unix(stream) => StdConn::Unix(stream.into_std()?),
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// One accepted connection en route to a shard: the detached stream, the
|
|
44
|
+
/// addresses hyper reports, and the slot it holds against max_connections.
|
|
45
|
+
struct Accepted {
|
|
46
|
+
conn: StdConn,
|
|
47
|
+
remote_addr: SocketAddr,
|
|
48
|
+
local_addr: SocketAddr,
|
|
49
|
+
permit: tokio::sync::OwnedSemaphorePermit,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Shard count: an explicit `io_threads` wins; the default is half the
|
|
53
|
+
/// available CPUs. Framing requests is cheap next to running the app, so
|
|
54
|
+
/// the I/O plane gets the smaller share and Ruby workers keep the rest.
|
|
55
|
+
pub(crate) fn thread_count(io_threads: usize) -> usize {
|
|
56
|
+
if io_threads > 0 {
|
|
57
|
+
return io_threads;
|
|
58
|
+
}
|
|
59
|
+
default_thread_count(std::thread::available_parallelism().map_or(1, usize::from))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn default_thread_count(cpus: usize) -> usize {
|
|
63
|
+
cpus.div_ceil(2)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Boot the shard threads, then the acceptor. Any thread that fails to
|
|
67
|
+
/// come up fails the whole boot: the already started threads are drained
|
|
68
|
+
/// (their senders drop) and joined before the error reaches Ruby.
|
|
69
|
+
pub(crate) fn spawn(
|
|
70
|
+
listener: Listener,
|
|
71
|
+
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
72
|
+
server: Arc<ServerInner>,
|
|
73
|
+
max_connections: usize,
|
|
74
|
+
accept_shutdown_rx: tokio::sync::watch::Receiver<bool>,
|
|
75
|
+
runtime_shutdown_rx: tokio::sync::watch::Receiver<bool>,
|
|
76
|
+
shard_count: usize,
|
|
77
|
+
) -> std::io::Result<Vec<JoinHandle<()>>> {
|
|
78
|
+
let shard_count = shard_count.max(1);
|
|
79
|
+
let mut handles = Vec::with_capacity(shard_count + 1);
|
|
80
|
+
let mut shard_txs = Vec::with_capacity(shard_count);
|
|
81
|
+
let mut loads = Vec::with_capacity(shard_count);
|
|
82
|
+
|
|
83
|
+
for i in 0..shard_count {
|
|
84
|
+
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
|
85
|
+
let load = Arc::new(AtomicUsize::new(0));
|
|
86
|
+
let spawned = spawn_shard(
|
|
87
|
+
i,
|
|
88
|
+
rx,
|
|
89
|
+
acceptor.clone(),
|
|
90
|
+
server.clone(),
|
|
91
|
+
load.clone(),
|
|
92
|
+
runtime_shutdown_rx.clone(),
|
|
93
|
+
);
|
|
94
|
+
match spawned {
|
|
95
|
+
Ok(handle) => {
|
|
96
|
+
shard_txs.push(tx);
|
|
97
|
+
loads.push(load);
|
|
98
|
+
handles.push(handle);
|
|
99
|
+
}
|
|
100
|
+
Err(error) => {
|
|
101
|
+
drop(tx);
|
|
102
|
+
drop(shard_txs);
|
|
103
|
+
join_all(handles);
|
|
104
|
+
return Err(error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
match spawn_acceptor(
|
|
110
|
+
listener,
|
|
111
|
+
server,
|
|
112
|
+
max_connections,
|
|
113
|
+
accept_shutdown_rx,
|
|
114
|
+
shard_txs,
|
|
115
|
+
loads,
|
|
116
|
+
) {
|
|
117
|
+
Ok(handle) => handles.push(handle),
|
|
118
|
+
Err(error) => {
|
|
119
|
+
join_all(handles);
|
|
120
|
+
return Err(error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
Ok(handles)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn join_all(handles: Vec<JoinHandle<()>>) {
|
|
127
|
+
for handle in handles {
|
|
128
|
+
let _ = handle.join();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn current_thread_runtime() -> std::io::Result<tokio::runtime::Runtime> {
|
|
133
|
+
tokio::runtime::Builder::new_current_thread()
|
|
134
|
+
.enable_all()
|
|
135
|
+
.build()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Wait for a just spawned I/O thread to report its runtime up, so a
|
|
139
|
+
/// startup failure becomes the boot error Ruby sees instead of a silently
|
|
140
|
+
/// dead thread.
|
|
141
|
+
fn await_ready(
|
|
142
|
+
handle: JoinHandle<()>,
|
|
143
|
+
ready_rx: std::sync::mpsc::Receiver<std::io::Result<()>>,
|
|
144
|
+
what: &str,
|
|
145
|
+
) -> std::io::Result<JoinHandle<()>> {
|
|
146
|
+
match ready_rx.recv() {
|
|
147
|
+
Ok(Ok(())) => Ok(handle),
|
|
148
|
+
Ok(Err(error)) => {
|
|
149
|
+
let _ = handle.join();
|
|
150
|
+
Err(error)
|
|
151
|
+
}
|
|
152
|
+
Err(_) => Err(std::io::Error::other(format!(
|
|
153
|
+
"{what} thread exited during startup"
|
|
154
|
+
))),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn spawn_shard(
|
|
159
|
+
index: usize,
|
|
160
|
+
rx: tokio::sync::mpsc::UnboundedReceiver<Accepted>,
|
|
161
|
+
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
162
|
+
server: Arc<ServerInner>,
|
|
163
|
+
load: Arc<AtomicUsize>,
|
|
164
|
+
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
|
|
165
|
+
) -> std::io::Result<JoinHandle<()>> {
|
|
166
|
+
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
|
|
167
|
+
let handle = std::thread::Builder::new()
|
|
168
|
+
.name(format!("kino-io-{index}"))
|
|
169
|
+
.spawn(move || {
|
|
170
|
+
let runtime = match current_thread_runtime() {
|
|
171
|
+
Ok(runtime) => runtime,
|
|
172
|
+
Err(error) => {
|
|
173
|
+
let _ = ready_tx.send(Err(error));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
let _ = ready_tx.send(Ok(()));
|
|
178
|
+
runtime.block_on(shard_loop(rx, acceptor, server, load, &mut shutdown_rx));
|
|
179
|
+
})?;
|
|
180
|
+
await_ready(handle, ready_rx, "shard")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fn spawn_acceptor(
|
|
184
|
+
listener: Listener,
|
|
185
|
+
server: Arc<ServerInner>,
|
|
186
|
+
max_connections: usize,
|
|
187
|
+
shutdown_rx: tokio::sync::watch::Receiver<bool>,
|
|
188
|
+
shard_txs: Vec<tokio::sync::mpsc::UnboundedSender<Accepted>>,
|
|
189
|
+
loads: Vec<Arc<AtomicUsize>>,
|
|
190
|
+
) -> std::io::Result<JoinHandle<()>> {
|
|
191
|
+
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
|
|
192
|
+
let handle = std::thread::Builder::new()
|
|
193
|
+
.name("kino-accept".to_string())
|
|
194
|
+
.spawn(move || {
|
|
195
|
+
let runtime = match current_thread_runtime() {
|
|
196
|
+
Ok(runtime) => runtime,
|
|
197
|
+
Err(error) => {
|
|
198
|
+
let _ = ready_tx.send(Err(error));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
runtime.block_on(async move {
|
|
203
|
+
// Registration must happen on this runtime; a failure is
|
|
204
|
+
// routed through the same ready channel as a build error.
|
|
205
|
+
let listener = match AsyncListener::from_std(listener) {
|
|
206
|
+
Ok(listener) => listener,
|
|
207
|
+
Err(error) => {
|
|
208
|
+
let _ = ready_tx.send(Err(error));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
accept_loop(
|
|
213
|
+
listener,
|
|
214
|
+
server,
|
|
215
|
+
max_connections,
|
|
216
|
+
shutdown_rx,
|
|
217
|
+
shard_txs,
|
|
218
|
+
loads,
|
|
219
|
+
ready_tx,
|
|
220
|
+
)
|
|
221
|
+
.await;
|
|
222
|
+
});
|
|
223
|
+
})?;
|
|
224
|
+
await_ready(handle, ready_rx, "accept")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Serve handed-over connections until the acceptor is gone, then let the
|
|
228
|
+
/// remaining ones finish. The final teardown signal cuts either phase
|
|
229
|
+
/// short: dropping the runtime cancels connection tasks at their next
|
|
230
|
+
/// await point.
|
|
231
|
+
async fn shard_loop(
|
|
232
|
+
mut rx: tokio::sync::mpsc::UnboundedReceiver<Accepted>,
|
|
233
|
+
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
234
|
+
server: Arc<ServerInner>,
|
|
235
|
+
load: Arc<AtomicUsize>,
|
|
236
|
+
shutdown_rx: &mut tokio::sync::watch::Receiver<bool>,
|
|
237
|
+
) {
|
|
238
|
+
let mut connections = tokio::task::JoinSet::new();
|
|
239
|
+
loop {
|
|
240
|
+
tokio::select! {
|
|
241
|
+
_ = shutdown_rx.changed() => return,
|
|
242
|
+
accepted = rx.recv() => {
|
|
243
|
+
let Some(accepted) = accepted else { break };
|
|
244
|
+
let acceptor = acceptor.clone();
|
|
245
|
+
let server = server.clone();
|
|
246
|
+
let guard = LoadGuard(load.clone());
|
|
247
|
+
connections.spawn(async move {
|
|
248
|
+
let _guard = guard;
|
|
249
|
+
serve_accepted(accepted, acceptor, server).await;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// Reap closed connections so the set stays small.
|
|
253
|
+
Some(_) = connections.join_next(), if !connections.is_empty() => {}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// The acceptor is gone: drain. Every join is one connection closing.
|
|
257
|
+
while !connections.is_empty() {
|
|
258
|
+
tokio::select! {
|
|
259
|
+
_ = shutdown_rx.changed() => return,
|
|
260
|
+
_ = connections.join_next() => {}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Keeps the shard's connection count honest whichever way the task ends:
|
|
266
|
+
/// return, panic, or cancellation at teardown. A plain decrement after the
|
|
267
|
+
/// await would never run on the last two.
|
|
268
|
+
struct LoadGuard(Arc<AtomicUsize>);
|
|
269
|
+
|
|
270
|
+
impl Drop for LoadGuard {
|
|
271
|
+
fn drop(&mut self) {
|
|
272
|
+
self.0.fetch_sub(1, Ordering::Relaxed);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// The shard's half of the handoff: re-register the stream on this
|
|
277
|
+
/// runtime, then run the shared connection pipeline (TLS handshake,
|
|
278
|
+
/// protocol layer) exactly as the default runtime would.
|
|
279
|
+
async fn serve_accepted(
|
|
280
|
+
accepted: Accepted,
|
|
281
|
+
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
282
|
+
server: Arc<ServerInner>,
|
|
283
|
+
) {
|
|
284
|
+
// Held for the connection's lifetime; dropping it frees a slot.
|
|
285
|
+
let _permit = accepted.permit;
|
|
286
|
+
let conn = match accepted.conn.into_tokio() {
|
|
287
|
+
Ok(conn) => conn,
|
|
288
|
+
Err(_) => {
|
|
289
|
+
log::emit(
|
|
290
|
+
Level::Warn,
|
|
291
|
+
"tokio",
|
|
292
|
+
"failed to register a stream on an I/O shard",
|
|
293
|
+
);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
serve_conn(
|
|
298
|
+
conn,
|
|
299
|
+
acceptor,
|
|
300
|
+
server,
|
|
301
|
+
accepted.remote_addr,
|
|
302
|
+
accepted.local_addr,
|
|
303
|
+
)
|
|
304
|
+
.await;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// The sharded accept loop. Same backpressure as the default loop: the
|
|
308
|
+
/// permit is acquired before accept, so past max_connections the excess
|
|
309
|
+
/// waits in the kernel backlog instead of being accepted and dropped.
|
|
310
|
+
/// A shard whose channel is gone is marked dead and routed around; with
|
|
311
|
+
/// no shard left the loop stops accepting and flips the server to
|
|
312
|
+
/// draining, so the control plane stops reporting ready.
|
|
313
|
+
async fn accept_loop(
|
|
314
|
+
listener: AsyncListener,
|
|
315
|
+
server: Arc<ServerInner>,
|
|
316
|
+
max_connections: usize,
|
|
317
|
+
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
|
|
318
|
+
shard_txs: Vec<tokio::sync::mpsc::UnboundedSender<Accepted>>,
|
|
319
|
+
loads: Vec<Arc<AtomicUsize>>,
|
|
320
|
+
ready_tx: std::sync::mpsc::SyncSender<std::io::Result<()>>,
|
|
321
|
+
) {
|
|
322
|
+
let conn_limit = Arc::new(tokio::sync::Semaphore::new(max_connections));
|
|
323
|
+
let mut live = vec![true; shard_txs.len()];
|
|
324
|
+
let _ = ready_tx.send(Ok(()));
|
|
325
|
+
'accept: loop {
|
|
326
|
+
let permit = tokio::select! {
|
|
327
|
+
_ = shutdown_rx.changed() => break,
|
|
328
|
+
permit = conn_limit.clone().acquire_owned() => match permit {
|
|
329
|
+
Ok(permit) => permit,
|
|
330
|
+
Err(_) => break,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
let (conn, remote_addr, local_addr) = tokio::select! {
|
|
334
|
+
_ = shutdown_rx.changed() => break,
|
|
335
|
+
accepted = listener.accept() => match accepted {
|
|
336
|
+
Ok(accepted) => accepted,
|
|
337
|
+
Err(_) => continue,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
let conn = match into_std(conn) {
|
|
341
|
+
Ok(conn) => conn,
|
|
342
|
+
Err(_) => {
|
|
343
|
+
log::emit(
|
|
344
|
+
Level::Warn,
|
|
345
|
+
"tokio",
|
|
346
|
+
"failed to detach an accepted stream; connection dropped",
|
|
347
|
+
);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
let mut accepted = Accepted {
|
|
352
|
+
conn,
|
|
353
|
+
remote_addr,
|
|
354
|
+
local_addr,
|
|
355
|
+
permit,
|
|
356
|
+
};
|
|
357
|
+
loop {
|
|
358
|
+
let Some(index) = least_loaded(&loads, &live) else {
|
|
359
|
+
log::emit(
|
|
360
|
+
Level::Error,
|
|
361
|
+
"tokio",
|
|
362
|
+
"all I/O shards are down; not accepting connections",
|
|
363
|
+
);
|
|
364
|
+
server.state.store(STATE_DRAINING, Ordering::Relaxed);
|
|
365
|
+
break 'accept;
|
|
366
|
+
};
|
|
367
|
+
loads[index].fetch_add(1, Ordering::Relaxed);
|
|
368
|
+
match shard_txs[index].send(accepted) {
|
|
369
|
+
Ok(()) => break,
|
|
370
|
+
Err(returned) => {
|
|
371
|
+
loads[index].fetch_sub(1, Ordering::Relaxed);
|
|
372
|
+
live[index] = false;
|
|
373
|
+
log::emit(Level::Warn, "tokio", "I/O shard is down; routing around it");
|
|
374
|
+
accepted = returned.0;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// The live shard with the fewest open connections.
|
|
382
|
+
fn least_loaded(loads: &[Arc<AtomicUsize>], live: &[bool]) -> Option<usize> {
|
|
383
|
+
loads
|
|
384
|
+
.iter()
|
|
385
|
+
.enumerate()
|
|
386
|
+
.filter(|(index, _)| live[*index])
|
|
387
|
+
.min_by_key(|(_, load)| load.load(Ordering::Relaxed))
|
|
388
|
+
.map(|(index, _)| index)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
#[cfg(test)]
|
|
392
|
+
mod tests {
|
|
393
|
+
use super::{default_thread_count, least_loaded, thread_count, Arc, AtomicUsize};
|
|
394
|
+
|
|
395
|
+
fn loads(counts: &[usize]) -> Vec<Arc<AtomicUsize>> {
|
|
396
|
+
counts
|
|
397
|
+
.iter()
|
|
398
|
+
.map(|&n| Arc::new(AtomicUsize::new(n)))
|
|
399
|
+
.collect()
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
#[test]
|
|
403
|
+
fn explicit_io_threads_win() {
|
|
404
|
+
assert_eq!(thread_count(3), 3);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#[test]
|
|
408
|
+
fn least_loaded_picks_the_emptiest_live_shard() {
|
|
409
|
+
assert_eq!(
|
|
410
|
+
least_loaded(&loads(&[3, 0, 1]), &[true, true, true]),
|
|
411
|
+
Some(1)
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#[test]
|
|
416
|
+
fn least_loaded_routes_around_dead_shards() {
|
|
417
|
+
// The dead shard's count is frozen at 0; it must not win anyway.
|
|
418
|
+
assert_eq!(
|
|
419
|
+
least_loaded(&loads(&[3, 0, 1]), &[true, false, true]),
|
|
420
|
+
Some(2)
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#[test]
|
|
425
|
+
fn least_loaded_reports_when_no_shard_is_left() {
|
|
426
|
+
assert_eq!(least_loaded(&loads(&[0, 0]), &[false, false]), None);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#[test]
|
|
430
|
+
fn default_is_half_the_cpus() {
|
|
431
|
+
assert_eq!(default_thread_count(1), 1);
|
|
432
|
+
assert_eq!(default_thread_count(2), 1);
|
|
433
|
+
assert_eq!(default_thread_count(3), 2);
|
|
434
|
+
assert_eq!(default_thread_count(12), 6);
|
|
435
|
+
assert_eq!(default_thread_count(128), 64);
|
|
436
|
+
}
|
|
437
|
+
}
|
data/ext/kino/src/lib.rs
CHANGED
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.
|
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
|
|
@@ -118,8 +164,8 @@ pub const LANE_DEPTH: usize = 4;
|
|
|
118
164
|
/// Fixed queue-wait bucket boundaries in microseconds (0.5ms .. 10s),
|
|
119
165
|
/// ascending. Emitted in seconds. Not a knob (YAGNI).
|
|
120
166
|
pub const QUEUE_BOUNDS_US: [u64; 14] = [
|
|
121
|
-
500, 1_000, 2_500, 5_000, 10_000, 25_000, 50_000, 100_000,
|
|
122
|
-
|
|
167
|
+
500, 1_000, 2_500, 5_000, 10_000, 25_000, 50_000, 100_000, 250_000, 500_000, 1_000_000,
|
|
168
|
+
2_500_000, 5_000_000, 10_000_000,
|
|
123
169
|
];
|
|
124
170
|
|
|
125
171
|
/// Queue-wait histogram: per-bucket counts plus an overflow (the implicit
|
|
@@ -289,7 +335,7 @@ pub fn test_server(lanes: bool, queue_depth: usize) -> Arc<ServerInner> {
|
|
|
289
335
|
req_tx: Mutex::new(Some(req_tx)),
|
|
290
336
|
req_rx,
|
|
291
337
|
shutdown_tx,
|
|
292
|
-
runtime: Mutex::new(None),
|
|
338
|
+
runtime: Mutex::new(RuntimeHandle::None),
|
|
293
339
|
slots: RwLock::new(Vec::new()),
|
|
294
340
|
in_flight: AtomicUsize::new(0),
|
|
295
341
|
served: AtomicU64::new(0),
|
|
@@ -301,7 +347,12 @@ pub fn test_server(lanes: bool, queue_depth: usize) -> Arc<ServerInner> {
|
|
|
301
347
|
state: std::sync::atomic::AtomicU8::new(STATE_BOOTING),
|
|
302
348
|
respawns: AtomicU64::new(0),
|
|
303
349
|
quarantine_replacements: AtomicU64::new(0),
|
|
304
|
-
topology: Topology {
|
|
350
|
+
topology: Topology {
|
|
351
|
+
mode: "threaded".to_string(),
|
|
352
|
+
workers: 0,
|
|
353
|
+
threads: 0,
|
|
354
|
+
batch: 1,
|
|
355
|
+
},
|
|
305
356
|
https: false,
|
|
306
357
|
unix_path: None,
|
|
307
358
|
access_log: None,
|
|
@@ -317,6 +368,44 @@ mod tests {
|
|
|
317
368
|
use super::*;
|
|
318
369
|
use crate::request::test_ctx;
|
|
319
370
|
|
|
371
|
+
#[test]
|
|
372
|
+
fn shards_shutdown_joins_threads_that_observe_the_signal() {
|
|
373
|
+
let (shutdown_tx, rx) = tokio::sync::watch::channel(false);
|
|
374
|
+
let thread = std::thread::spawn(move || {
|
|
375
|
+
while !*rx.borrow() {
|
|
376
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
let start = Instant::now();
|
|
381
|
+
RuntimeHandle::Shards {
|
|
382
|
+
shutdown_tx,
|
|
383
|
+
threads: vec![thread],
|
|
384
|
+
}
|
|
385
|
+
.shutdown(Duration::from_secs(5));
|
|
386
|
+
|
|
387
|
+
// The thread exits on the signal, so the join comes nowhere near
|
|
388
|
+
// the deadline.
|
|
389
|
+
assert!(start.elapsed() < Duration::from_secs(1));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#[test]
|
|
393
|
+
fn shards_shutdown_abandons_a_wedged_thread_at_the_deadline() {
|
|
394
|
+
let (shutdown_tx, _rx) = tokio::sync::watch::channel(false);
|
|
395
|
+
let wedged = std::thread::spawn(|| std::thread::sleep(Duration::from_secs(30)));
|
|
396
|
+
|
|
397
|
+
let start = Instant::now();
|
|
398
|
+
RuntimeHandle::Shards {
|
|
399
|
+
shutdown_tx,
|
|
400
|
+
threads: vec![wedged],
|
|
401
|
+
}
|
|
402
|
+
.shutdown(Duration::from_millis(50));
|
|
403
|
+
|
|
404
|
+
let elapsed = start.elapsed();
|
|
405
|
+
assert!(elapsed >= Duration::from_millis(50));
|
|
406
|
+
assert!(elapsed < Duration::from_secs(5));
|
|
407
|
+
}
|
|
408
|
+
|
|
320
409
|
#[test]
|
|
321
410
|
fn worker_registration_hands_out_sequential_slot_ids() {
|
|
322
411
|
let server = test_server(false, 4);
|
|
@@ -420,9 +509,9 @@ mod tests {
|
|
|
420
509
|
#[test]
|
|
421
510
|
fn queue_histogram_buckets_by_wait() {
|
|
422
511
|
let h = QueueHistogram::new();
|
|
423
|
-
h.record(400);
|
|
424
|
-
h.record(500);
|
|
425
|
-
h.record(600);
|
|
512
|
+
h.record(400); // <= 500 -> bucket 0
|
|
513
|
+
h.record(500); // == 500 -> bucket 0 (inclusive)
|
|
514
|
+
h.record(600); // (500, 1000] -> bucket 1
|
|
426
515
|
h.record(20_000_000); // > last bound -> overflow
|
|
427
516
|
let s = h.snapshot();
|
|
428
517
|
assert_eq!(s.buckets[0], 2);
|
data/ext/kino/src/response.rs
CHANGED
|
@@ -115,10 +115,7 @@ pub fn plain_response(status: u16, message: &'static str) -> HyperResponse {
|
|
|
115
115
|
mod tests {
|
|
116
116
|
use super::*;
|
|
117
117
|
|
|
118
|
-
fn pair() -> (
|
|
119
|
-
Responder,
|
|
120
|
-
tokio::sync::oneshot::Receiver<HyperResponse>,
|
|
121
|
-
) {
|
|
118
|
+
fn pair() -> (Responder, tokio::sync::oneshot::Receiver<HyperResponse>) {
|
|
122
119
|
let (head_tx, head_rx) = tokio::sync::oneshot::channel();
|
|
123
120
|
(Responder::new(head_tx), head_rx)
|
|
124
121
|
}
|