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
|
@@ -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
|
@@ -4,9 +4,14 @@
|
|
|
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 io_shards;
|
|
13
|
+
mod listen;
|
|
14
|
+
mod log;
|
|
10
15
|
mod logsink;
|
|
11
16
|
mod mono;
|
|
12
17
|
mod pin;
|
|
@@ -62,8 +67,12 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
62
67
|
function!(server::interrupt_all_workers, 1),
|
|
63
68
|
)?;
|
|
64
69
|
native.define_singleton_method("shutdown_runtime", function!(server::shutdown_runtime, 2))?;
|
|
65
|
-
native.define_singleton_method("
|
|
70
|
+
native.define_singleton_method("log_line", function!(log::log_line, 3))?;
|
|
66
71
|
native.define_singleton_method("sleep_chunk", function!(timer::sleep_chunk, 1))?;
|
|
72
|
+
native.define_singleton_method(
|
|
73
|
+
"available_parallelism",
|
|
74
|
+
function!(cpus::available_parallelism, 0),
|
|
75
|
+
)?;
|
|
67
76
|
native.define_singleton_method("log_device_open", function!(logsink::device_open, 1))?;
|
|
68
77
|
native.define_singleton_method("log_device_write", function!(logsink::device_write, 2))?;
|
|
69
78
|
native.define_singleton_method("log_device_close", function!(logsink::device_close, 1))?;
|
|
@@ -87,6 +96,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
87
96
|
request.define_method("write_chunk", method!(Request::write_chunk, 1))?;
|
|
88
97
|
request.define_method("finish", method!(Request::finish, 0))?;
|
|
89
98
|
request.define_method("abort", method!(Request::abort, 0))?;
|
|
99
|
+
request.define_method("timing", method!(Request::set_timing, 2))?;
|
|
90
100
|
|
|
91
101
|
// Force-resolve the TypedData class cache on the main ractor: magnus
|
|
92
102
|
// resolves it lazily on first wrap, and a racy first resolution from two
|
|
@@ -0,0 +1,137 @@
|
|
|
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!(
|
|
90
|
+
unix_path("unix:///run/kino.sock").unwrap().to_str(),
|
|
91
|
+
Some("/run/kino.sock")
|
|
92
|
+
);
|
|
93
|
+
assert!(unix_path("127.0.0.1").is_none());
|
|
94
|
+
assert!(unix_path("unix.example.com").is_none());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
fn binds_a_unix_socket_and_reports_no_port() {
|
|
99
|
+
let path = socket_path("bind");
|
|
100
|
+
let listener = Listener::bind(&format!("unix://{}", path.display()), 9292).unwrap();
|
|
101
|
+
assert!(matches!(listener, Listener::Unix(..)));
|
|
102
|
+
assert_eq!(listener.port().unwrap(), 0);
|
|
103
|
+
assert!(std::fs::metadata(&path).is_ok());
|
|
104
|
+
drop(listener);
|
|
105
|
+
let _ = std::fs::remove_file(&path);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn reclaims_a_stale_socket_file() {
|
|
110
|
+
let path = socket_path("stale");
|
|
111
|
+
// Dropping a listener closes the socket but leaves its file behind,
|
|
112
|
+
// exactly what a crashed process leaves.
|
|
113
|
+
drop(bind_unix(&path).unwrap());
|
|
114
|
+
assert!(std::fs::metadata(&path).is_ok());
|
|
115
|
+
let reclaimed = bind_unix(&path).unwrap();
|
|
116
|
+
drop(reclaimed);
|
|
117
|
+
let _ = std::fs::remove_file(&path);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[test]
|
|
121
|
+
fn refuses_a_socket_someone_is_listening_on() {
|
|
122
|
+
let path = socket_path("live");
|
|
123
|
+
let live = bind_unix(&path).unwrap();
|
|
124
|
+
let err = bind_unix(&path).unwrap_err();
|
|
125
|
+
assert_eq!(err.kind(), std::io::ErrorKind::AddrInUse);
|
|
126
|
+
assert!(err.to_string().contains("in use"));
|
|
127
|
+
drop(live);
|
|
128
|
+
let _ = std::fs::remove_file(&path);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn binds_tcp_on_an_ephemeral_port() {
|
|
133
|
+
let listener = Listener::bind("127.0.0.1", 0).unwrap();
|
|
134
|
+
assert!(matches!(listener, Listener::Tcp(_)));
|
|
135
|
+
assert_ne!(listener.port().unwrap(), 0);
|
|
136
|
+
}
|
|
137
|
+
}
|