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
data/ext/kino/src/server.rs
CHANGED
|
@@ -14,7 +14,7 @@ use magnus::{Error, Ruby};
|
|
|
14
14
|
use parking_lot::{Mutex, RwLock};
|
|
15
15
|
|
|
16
16
|
use crate::listen::Listener;
|
|
17
|
-
use crate::registry::{self, BoxedCtx, ServerInner, WorkerSlot};
|
|
17
|
+
use crate::registry::{self, BoxedCtx, RuntimeHandle, ServerInner, WorkerSlot};
|
|
18
18
|
use crate::request::RequestCtx;
|
|
19
19
|
use crate::response::{plain_response, HyperResponse, Responder};
|
|
20
20
|
|
|
@@ -56,6 +56,8 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
56
56
|
let request_timeout_ms: u64 = cfg_opt::<u64>(ruby, config, "request_timeout_ms")?.unwrap_or(0);
|
|
57
57
|
let max_body_size: usize = cfg_opt::<usize>(ruby, config, "max_body_size")?.unwrap_or(0);
|
|
58
58
|
let max_connections: usize = cfg_opt::<usize>(ruby, config, "max_connections")?.unwrap_or(1024);
|
|
59
|
+
let io_shards: bool = cfg_opt(ruby, config, "io_shards")?.unwrap_or(false);
|
|
60
|
+
let io_threads: usize = cfg_opt::<usize>(ruby, config, "io_threads")?.unwrap_or(0);
|
|
59
61
|
let tokio_threads: usize = cfg_opt::<usize>(ruby, config, "tokio_threads")?.unwrap_or(0);
|
|
60
62
|
let tls_cert: Option<String> = cfg_opt(ruby, config, "tls_cert")?;
|
|
61
63
|
let tls_key: Option<String> = cfg_opt(ruby, config, "tls_key")?;
|
|
@@ -79,8 +81,7 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
79
81
|
}
|
|
80
82
|
};
|
|
81
83
|
|
|
82
|
-
let listener =
|
|
83
|
-
Listener::bind(&bind, port).map_err(|e| io_error(ruby, "bind failed", e))?;
|
|
84
|
+
let listener = Listener::bind(&bind, port).map_err(|e| io_error(ruby, "bind failed", e))?;
|
|
84
85
|
// Ruby refuses this combination up front; this guards embedders
|
|
85
86
|
// calling the native layer directly.
|
|
86
87
|
if acceptor.is_some() && matches!(listener, Listener::Unix(..)) {
|
|
@@ -106,15 +107,6 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
106
107
|
})
|
|
107
108
|
.transpose()?;
|
|
108
109
|
|
|
109
|
-
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
|
110
|
-
builder.enable_all().thread_name("kino-tokio");
|
|
111
|
-
if tokio_threads > 0 {
|
|
112
|
-
builder.worker_threads(tokio_threads);
|
|
113
|
-
}
|
|
114
|
-
let runtime = builder
|
|
115
|
-
.build()
|
|
116
|
-
.map_err(|e| io_error(ruby, "tokio runtime failed", e))?;
|
|
117
|
-
|
|
118
110
|
let (req_tx, req_rx) = flume::bounded(queue_depth);
|
|
119
111
|
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
|
|
120
112
|
|
|
@@ -123,7 +115,7 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
123
115
|
req_tx: Mutex::new(Some(req_tx)),
|
|
124
116
|
req_rx,
|
|
125
117
|
shutdown_tx,
|
|
126
|
-
runtime: Mutex::new(None),
|
|
118
|
+
runtime: Mutex::new(RuntimeHandle::None),
|
|
127
119
|
slots: RwLock::new(Vec::new()),
|
|
128
120
|
in_flight: std::sync::atomic::AtomicUsize::new(0),
|
|
129
121
|
served: std::sync::atomic::AtomicU64::new(0),
|
|
@@ -135,7 +127,12 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
135
127
|
state: std::sync::atomic::AtomicU8::new(registry::STATE_BOOTING),
|
|
136
128
|
respawns: std::sync::atomic::AtomicU64::new(0),
|
|
137
129
|
quarantine_replacements: std::sync::atomic::AtomicU64::new(0),
|
|
138
|
-
topology: registry::Topology {
|
|
130
|
+
topology: registry::Topology {
|
|
131
|
+
mode,
|
|
132
|
+
workers,
|
|
133
|
+
threads,
|
|
134
|
+
batch,
|
|
135
|
+
},
|
|
139
136
|
https: acceptor.is_some(),
|
|
140
137
|
unix_path,
|
|
141
138
|
access_log: log_requests.then(|| crate::logsink::Sink::new(std::io::stdout())),
|
|
@@ -145,18 +142,48 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
145
142
|
queue_histogram: registry::QueueHistogram::new(),
|
|
146
143
|
});
|
|
147
144
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
145
|
+
if io_shards {
|
|
146
|
+
// The shards keep serving accepted connections while the acceptor
|
|
147
|
+
// drains on `shutdown_rx`; this second signal stops them only at
|
|
148
|
+
// final teardown.
|
|
149
|
+
let (runtime_shutdown_tx, runtime_shutdown_rx) = tokio::sync::watch::channel(false);
|
|
150
|
+
let threads = crate::io_shards::spawn(
|
|
151
|
+
listener,
|
|
152
|
+
acceptor,
|
|
153
|
+
server.clone(),
|
|
154
|
+
max_connections,
|
|
155
|
+
shutdown_rx,
|
|
156
|
+
runtime_shutdown_rx,
|
|
157
|
+
crate::io_shards::thread_count(io_threads),
|
|
158
|
+
)
|
|
159
|
+
.map_err(|e| io_error(ruby, "sharded runtime failed", e))?;
|
|
160
|
+
*server.runtime.lock() = RuntimeHandle::Shards {
|
|
161
|
+
shutdown_tx: runtime_shutdown_tx,
|
|
162
|
+
threads,
|
|
163
|
+
};
|
|
164
|
+
} else {
|
|
165
|
+
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
|
166
|
+
builder.enable_all().thread_name("kino-tokio");
|
|
167
|
+
if tokio_threads > 0 {
|
|
168
|
+
builder.worker_threads(tokio_threads);
|
|
169
|
+
}
|
|
170
|
+
let runtime = builder
|
|
171
|
+
.build()
|
|
172
|
+
.map_err(|e| io_error(ruby, "tokio runtime failed", e))?;
|
|
173
|
+
let tokio_listener = {
|
|
174
|
+
let _guard = runtime.enter();
|
|
175
|
+
AsyncListener::from_std(listener)
|
|
176
|
+
.map_err(|e| io_error(ruby, "listener setup failed", e))?
|
|
177
|
+
};
|
|
178
|
+
runtime.spawn(accept_loop(
|
|
179
|
+
tokio_listener,
|
|
180
|
+
acceptor,
|
|
181
|
+
server.clone(),
|
|
182
|
+
max_connections,
|
|
183
|
+
shutdown_rx,
|
|
184
|
+
));
|
|
185
|
+
*server.runtime.lock() = RuntimeHandle::MultiThread(runtime);
|
|
186
|
+
}
|
|
160
187
|
|
|
161
188
|
let id = server.id;
|
|
162
189
|
let control_port = match control_bind {
|
|
@@ -170,10 +197,11 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16, Opt
|
|
|
170
197
|
Some(bind) => match crate::control::start(bind, server.clone(), control_token) {
|
|
171
198
|
Ok(port) => port,
|
|
172
199
|
Err(e) => {
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
200
|
+
// Nothing is serving yet, so the bound only matters for a
|
|
201
|
+
// wedged shard thread; the default runtime just cancels
|
|
202
|
+
// its one idle accept task.
|
|
203
|
+
let _ = server.shutdown_tx.send(true);
|
|
204
|
+
std::mem::take(&mut *server.runtime.lock()).shutdown(Duration::from_millis(1_000));
|
|
177
205
|
return Err(io_error(ruby, "control start failed", e));
|
|
178
206
|
}
|
|
179
207
|
},
|
|
@@ -200,22 +228,24 @@ const UNIX_LOCAL: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST),
|
|
|
200
228
|
|
|
201
229
|
/// The accept loop's listener: TCP (optionally behind TLS), or a unix
|
|
202
230
|
/// socket, which carries plain HTTP only.
|
|
203
|
-
enum AsyncListener {
|
|
231
|
+
pub(crate) enum AsyncListener {
|
|
204
232
|
Tcp(tokio::net::TcpListener),
|
|
205
233
|
Unix(tokio::net::UnixListener),
|
|
206
234
|
}
|
|
207
235
|
|
|
208
236
|
/// One accepted connection, before the protocol layer sees it.
|
|
209
|
-
enum Conn {
|
|
237
|
+
pub(crate) enum Conn {
|
|
210
238
|
Tcp(tokio::net::TcpStream),
|
|
211
239
|
Unix(tokio::net::UnixStream),
|
|
212
240
|
}
|
|
213
241
|
|
|
214
242
|
impl AsyncListener {
|
|
215
243
|
/// Register the bound listener with the current runtime.
|
|
216
|
-
fn from_std(listener: Listener) -> std::io::Result<AsyncListener> {
|
|
244
|
+
pub(crate) fn from_std(listener: Listener) -> std::io::Result<AsyncListener> {
|
|
217
245
|
Ok(match listener {
|
|
218
|
-
Listener::Tcp(listener) =>
|
|
246
|
+
Listener::Tcp(listener) => {
|
|
247
|
+
AsyncListener::Tcp(tokio::net::TcpListener::from_std(listener)?)
|
|
248
|
+
}
|
|
219
249
|
Listener::Unix(listener, _) => {
|
|
220
250
|
AsyncListener::Unix(tokio::net::UnixListener::from_std(listener)?)
|
|
221
251
|
}
|
|
@@ -223,7 +253,7 @@ impl AsyncListener {
|
|
|
223
253
|
}
|
|
224
254
|
|
|
225
255
|
/// The next connection with its (peer, local) addresses.
|
|
226
|
-
async fn accept(&self) -> std::io::Result<(Conn, SocketAddr, SocketAddr)> {
|
|
256
|
+
pub(crate) async fn accept(&self) -> std::io::Result<(Conn, SocketAddr, SocketAddr)> {
|
|
227
257
|
match self {
|
|
228
258
|
AsyncListener::Tcp(listener) => {
|
|
229
259
|
let (stream, remote_addr) = listener.accept().await?;
|
|
@@ -274,27 +304,38 @@ async fn accept_loop(
|
|
|
274
304
|
tokio::spawn(async move {
|
|
275
305
|
// Held for the connection's lifetime; dropping it frees a slot.
|
|
276
306
|
let _permit = permit;
|
|
277
|
-
|
|
278
|
-
(Conn::Tcp(stream), Some(acceptor)) => {
|
|
279
|
-
// Handshake failures (port scans, plain HTTP to a TLS
|
|
280
|
-
// port) and stalled handshakes (slowloris) just drop the
|
|
281
|
-
// connection; the timeout bounds the latter.
|
|
282
|
-
let handshake = tokio::time::timeout(TLS_HANDSHAKE_TIMEOUT, acceptor.accept(stream));
|
|
283
|
-
let Ok(Ok(tls)) = handshake.await else { return };
|
|
284
|
-
serve_connection(tls, server, remote_addr, local_addr).await;
|
|
285
|
-
}
|
|
286
|
-
(Conn::Tcp(stream), None) => {
|
|
287
|
-
serve_connection(stream, server, remote_addr, local_addr).await
|
|
288
|
-
}
|
|
289
|
-
// TLS over a unix socket is refused at bind time.
|
|
290
|
-
(Conn::Unix(stream), _) => {
|
|
291
|
-
serve_connection(stream, server, remote_addr, local_addr).await
|
|
292
|
-
}
|
|
293
|
-
}
|
|
307
|
+
serve_conn(conn, acceptor, server, remote_addr, local_addr).await;
|
|
294
308
|
});
|
|
295
309
|
}
|
|
296
310
|
}
|
|
297
311
|
|
|
312
|
+
/// Everything between an accepted connection and hyper: the optional TLS
|
|
313
|
+
/// handshake, then the protocol layer. Shared by the default accept loop
|
|
314
|
+
/// and the sharded one, so connection policy exists exactly once.
|
|
315
|
+
pub(crate) async fn serve_conn(
|
|
316
|
+
conn: Conn,
|
|
317
|
+
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
318
|
+
server: Arc<ServerInner>,
|
|
319
|
+
remote_addr: SocketAddr,
|
|
320
|
+
local_addr: SocketAddr,
|
|
321
|
+
) {
|
|
322
|
+
match (conn, acceptor) {
|
|
323
|
+
(Conn::Tcp(stream), Some(acceptor)) => {
|
|
324
|
+
// Handshake failures (port scans, plain HTTP to a TLS
|
|
325
|
+
// port) and stalled handshakes (slowloris) just drop the
|
|
326
|
+
// connection; the timeout bounds the latter.
|
|
327
|
+
let handshake = tokio::time::timeout(TLS_HANDSHAKE_TIMEOUT, acceptor.accept(stream));
|
|
328
|
+
let Ok(Ok(tls)) = handshake.await else { return };
|
|
329
|
+
serve_connection(tls, server, remote_addr, local_addr).await;
|
|
330
|
+
}
|
|
331
|
+
(Conn::Tcp(stream), None) => {
|
|
332
|
+
serve_connection(stream, server, remote_addr, local_addr).await
|
|
333
|
+
}
|
|
334
|
+
// TLS over a unix socket is refused at bind time.
|
|
335
|
+
(Conn::Unix(stream), _) => serve_connection(stream, server, remote_addr, local_addr).await,
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
298
339
|
/// Slowloris guard: drop a connection that has not sent its complete request
|
|
299
340
|
/// headers within this window. Long enough never to trip a real client (even
|
|
300
341
|
/// on a slow mobile link), short enough to reap a stalled one. Deliberately a
|
|
@@ -380,7 +421,11 @@ async fn handle_request(
|
|
|
380
421
|
None => parts.uri.path().to_string(),
|
|
381
422
|
};
|
|
382
423
|
let method = parts.method.to_string();
|
|
383
|
-
log.write_line(crate::access_log::arrival(
|
|
424
|
+
log.write_line(crate::access_log::arrival(
|
|
425
|
+
&method,
|
|
426
|
+
&target,
|
|
427
|
+
remote_addr.ip(),
|
|
428
|
+
));
|
|
384
429
|
(std::time::Instant::now(), method, target)
|
|
385
430
|
});
|
|
386
431
|
|
|
@@ -627,7 +672,9 @@ pub fn register_worker(ruby: &Ruby, server_id: u64) -> Result<usize, Error> {
|
|
|
627
672
|
|
|
628
673
|
pub fn stop_accepting(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
629
674
|
if let Some(server) = registry::try_get(server_id) {
|
|
630
|
-
server
|
|
675
|
+
server
|
|
676
|
+
.state
|
|
677
|
+
.store(registry::STATE_DRAINING, Ordering::Relaxed);
|
|
631
678
|
let _ = server.shutdown_tx.send(true);
|
|
632
679
|
}
|
|
633
680
|
Ok(())
|
|
@@ -716,9 +763,8 @@ pub fn interrupt_all_workers(_ruby: &Ruby, server_id: u64) -> Result<(), Error>
|
|
|
716
763
|
|
|
717
764
|
pub fn shutdown_runtime(_ruby: &Ruby, server_id: u64, timeout_ms: u64) -> Result<(), Error> {
|
|
718
765
|
if let Some(server) = registry::remove(server_id) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
766
|
+
let _ = server.shutdown_tx.send(true);
|
|
767
|
+
std::mem::take(&mut *server.runtime.lock()).shutdown(Duration::from_millis(timeout_ms));
|
|
722
768
|
// The listener is closed with the runtime; its socket file is not.
|
|
723
769
|
if let Some(path) = &server.unix_path {
|
|
724
770
|
crate::listen::cleanup_unix(path);
|
|
@@ -775,10 +821,7 @@ pub type WorkerStatRow = (usize, u64, usize, u64, bool);
|
|
|
775
821
|
|
|
776
822
|
/// Per-slot rows for Server#stats parity: [index, served, in_flight,
|
|
777
823
|
/// busy_ms, quarantined] each. Empty when the server is gone.
|
|
778
|
-
pub fn worker_stats(
|
|
779
|
-
_ruby: &Ruby,
|
|
780
|
-
server_id: u64,
|
|
781
|
-
) -> Result<Vec<WorkerStatRow>, Error> {
|
|
824
|
+
pub fn worker_stats(_ruby: &Ruby, server_id: u64) -> Result<Vec<WorkerStatRow>, Error> {
|
|
782
825
|
let Some(server) = registry::try_get(server_id) else {
|
|
783
826
|
return Ok(Vec::new());
|
|
784
827
|
};
|
|
@@ -800,7 +843,9 @@ pub fn quarantine_slot(ruby: &Ruby, server_id: u64, worker_id: usize) -> Result<
|
|
|
800
843
|
/// One replacement spawned by the quarantine monitor.
|
|
801
844
|
pub fn record_quarantine_replacement(_ruby: &Ruby, server_id: u64) -> Result<(), Error> {
|
|
802
845
|
if let Some(server) = registry::try_get(server_id) {
|
|
803
|
-
server
|
|
846
|
+
server
|
|
847
|
+
.quarantine_replacements
|
|
848
|
+
.fetch_add(1, Ordering::Relaxed);
|
|
804
849
|
}
|
|
805
850
|
Ok(())
|
|
806
851
|
}
|
|
@@ -851,7 +896,10 @@ mod tests {
|
|
|
851
896
|
server.register_worker();
|
|
852
897
|
server.slots.read()[0].lane_tx.lock().take();
|
|
853
898
|
|
|
854
|
-
assert!(matches!(
|
|
899
|
+
assert!(matches!(
|
|
900
|
+
try_dispatch(&server, test_ctx()),
|
|
901
|
+
Dispatch::Closed
|
|
902
|
+
));
|
|
855
903
|
}
|
|
856
904
|
|
|
857
905
|
#[test]
|
|
@@ -894,9 +942,7 @@ mod tests {
|
|
|
894
942
|
let server = test_server(true, 4);
|
|
895
943
|
server.register_worker();
|
|
896
944
|
server.register_worker();
|
|
897
|
-
server.slots.read()[0]
|
|
898
|
-
.parked
|
|
899
|
-
.store(true, Ordering::Relaxed);
|
|
945
|
+
server.slots.read()[0].parked.store(true, Ordering::Relaxed);
|
|
900
946
|
|
|
901
947
|
// Both dispatches land on the awake lane (slot 1), regardless of
|
|
902
948
|
// where the rotating cursor starts.
|
data/ext/kino/src/tls.rs
CHANGED
|
@@ -71,7 +71,8 @@ WJ2lRijROyX9v7f8aSQlb6kEwKhI8kG8SbeUc+zbKkzGgRXNaZHY/mAa
|
|
|
71
71
|
#[test]
|
|
72
72
|
fn missing_file_paths_error_without_panicking() {
|
|
73
73
|
let err = build_acceptor("/nonexistent/cert.pem", "/nonexistent/key.pem")
|
|
74
|
-
.err()
|
|
74
|
+
.err()
|
|
75
|
+
.expect("missing files");
|
|
75
76
|
assert!(err.contains("cannot read"));
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -83,14 +84,20 @@ WJ2lRijROyX9v7f8aSQlb6kEwKhI8kG8SbeUc+zbKkzGgRXNaZHY/mAa
|
|
|
83
84
|
|
|
84
85
|
#[test]
|
|
85
86
|
fn pem_without_a_key_is_rejected() {
|
|
86
|
-
let err = build_acceptor(CERT, CERT)
|
|
87
|
+
let err = build_acceptor(CERT, CERT)
|
|
88
|
+
.err()
|
|
89
|
+
.expect("a cert is not a key");
|
|
87
90
|
assert!(err.contains("no private key found"));
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
#[test]
|
|
91
94
|
fn mismatched_cert_and_garbage_key_are_rejected() {
|
|
92
|
-
let err = build_acceptor(
|
|
93
|
-
|
|
95
|
+
let err = build_acceptor(
|
|
96
|
+
CERT,
|
|
97
|
+
"-----BEGIN PRIVATE KEY-----\ngarbage\n-----END PRIVATE KEY-----",
|
|
98
|
+
)
|
|
99
|
+
.err()
|
|
100
|
+
.expect("garbage key");
|
|
94
101
|
assert!(!err.is_empty());
|
|
95
102
|
}
|
|
96
103
|
}
|
data/lib/kino/configuration.rb
CHANGED
|
@@ -26,6 +26,8 @@ module Kino
|
|
|
26
26
|
after_request_complete: nil,
|
|
27
27
|
on_worker_exit: nil,
|
|
28
28
|
shutdown_timeout: 30,
|
|
29
|
+
io_shards: false,
|
|
30
|
+
io_threads: nil,
|
|
29
31
|
tokio_threads: nil,
|
|
30
32
|
tls: nil,
|
|
31
33
|
environment: nil,
|
|
@@ -148,6 +150,8 @@ module Kino
|
|
|
148
150
|
# queue_depth 2048
|
|
149
151
|
# queue_timeout 0.5
|
|
150
152
|
# shutdown_timeout 15
|
|
153
|
+
# io_shards true
|
|
154
|
+
# io_threads 6
|
|
151
155
|
# tokio_threads 4
|
|
152
156
|
# tls cert: "cert.pem", key: "key.pem"
|
|
153
157
|
#
|
|
@@ -229,6 +233,12 @@ module Kino
|
|
|
229
233
|
# Graceful-shutdown drain deadline in seconds.
|
|
230
234
|
def shutdown_timeout(seconds) = @config.set(:shutdown_timeout, seconds)
|
|
231
235
|
|
|
236
|
+
# Run native HTTP I/O on current-thread shards instead of Tokio's shared pool.
|
|
237
|
+
def io_shards(enabled = true) = @config.set(:io_shards, !!enabled)
|
|
238
|
+
|
|
239
|
+
# Native HTTP I/O shard count; default with io_shards: half available CPUs.
|
|
240
|
+
def io_threads(count) = @config.set(:io_threads, Integer(count))
|
|
241
|
+
|
|
232
242
|
# Threads for the tokio (Rust I/O) runtime; default: one per core.
|
|
233
243
|
def tokio_threads(count) = @config.set(:tokio_threads, Integer(count))
|
|
234
244
|
|
data/lib/kino/server.rb
CHANGED
|
@@ -98,6 +98,12 @@ module Kino
|
|
|
98
98
|
@lanes = !!settings[:lanes]
|
|
99
99
|
@log_requests = !!settings[:log_requests]
|
|
100
100
|
@shutdown_timeout = settings[:shutdown_timeout]
|
|
101
|
+
@io_shards = !!settings[:io_shards]
|
|
102
|
+
@io_threads = Integer(settings[:io_threads]) unless settings[:io_threads].nil?
|
|
103
|
+
if @io_threads && @io_threads < 1
|
|
104
|
+
raise ArgumentError, "io_threads must be >= 1"
|
|
105
|
+
end
|
|
106
|
+
Log.warn("io_threads has no effect unless io_shards is true") if @io_threads && !@io_shards
|
|
101
107
|
@tokio_threads = settings[:tokio_threads]
|
|
102
108
|
@tls = validate_tls(settings[:tls])
|
|
103
109
|
if @tls && unix?
|
|
@@ -145,6 +151,8 @@ module Kino
|
|
|
145
151
|
request_timeout_ms: @request_timeout_ms,
|
|
146
152
|
max_connections: @max_connections,
|
|
147
153
|
max_body_size: @max_body_size,
|
|
154
|
+
io_shards: @io_shards,
|
|
155
|
+
io_threads: @io_threads,
|
|
148
156
|
tokio_threads: @tokio_threads,
|
|
149
157
|
tls_cert: @tls&.fetch(:cert), tls_key: @tls&.fetch(:key),
|
|
150
158
|
lanes: @lanes, log_requests: @log_requests,
|
|
@@ -125,8 +125,14 @@
|
|
|
125
125
|
|
|
126
126
|
## Runtime
|
|
127
127
|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
128
|
+
# Run native HTTP I/O on current-thread shards instead of Tokio's shared
|
|
129
|
+
# worker pool, reducing scheduler contention on very fast handlers.
|
|
130
|
+
# io_shards true
|
|
131
|
+
|
|
132
|
+
# I/O shard count. Default with io_shards: half available CPUs.
|
|
133
|
+
# io_threads 6
|
|
134
|
+
|
|
135
|
+
# Threads for the Tokio multi-thread runtime. Default: one per available CPU.
|
|
130
136
|
# tokio_threads 4
|
|
131
137
|
|
|
132
138
|
## Control plane
|
data/lib/kino/version.rb
CHANGED
data/sig/kino.rbs
CHANGED
|
@@ -126,6 +126,8 @@ module Kino
|
|
|
126
126
|
def after_request_complete: (?^(Hash[String, untyped], Integer) -> void handler) ?{ (Hash[String, untyped], Integer) -> void } -> untyped
|
|
127
127
|
def on_worker_exit: (?^(Integer, Exception?) -> void handler) ?{ (Integer, Exception?) -> void } -> untyped
|
|
128
128
|
def shutdown_timeout: (Numeric seconds) -> untyped
|
|
129
|
+
def io_shards: (?boolish enabled) -> untyped
|
|
130
|
+
def io_threads: (int? count) -> untyped
|
|
129
131
|
def tokio_threads: (int count) -> untyped
|
|
130
132
|
def tls: (cert: String, key: String) -> untyped
|
|
131
133
|
def environment: (String | Symbol env) -> untyped
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kino
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Markin
|
|
@@ -180,6 +180,7 @@ files:
|
|
|
180
180
|
- ext/kino/src/cpus.rs
|
|
181
181
|
- ext/kino/src/env_strings.rs
|
|
182
182
|
- ext/kino/src/gvl.rs
|
|
183
|
+
- ext/kino/src/io_shards.rs
|
|
183
184
|
- ext/kino/src/lib.rs
|
|
184
185
|
- ext/kino/src/listen.rs
|
|
185
186
|
- ext/kino/src/log.rs
|