kino 0.1.0 → 0.1.2
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 +37 -0
- data/Cargo.lock +1 -1
- data/README.md +103 -57
- data/doc/benchmarks.md +208 -89
- data/doc/rails-on-ractors.md +5 -4
- data/doc/why-kino.md +8 -8
- data/ext/kino/Cargo.toml +1 -1
- data/ext/kino/src/registry.rs +4 -0
- data/ext/kino/src/request.rs +33 -1
- data/ext/kino/src/server.rs +123 -25
- data/lib/kino/configuration.rb +14 -3
- data/lib/kino/server.rb +21 -1
- data/lib/kino/templates/kino.rb.tt +63 -83
- data/lib/kino/version.rb +1 -1
- data/sig/kino.rbs +2 -0
- metadata +1 -1
data/ext/kino/src/server.rs
CHANGED
|
@@ -51,6 +51,8 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16), Er
|
|
|
51
51
|
let queue_depth: usize = cfg(ruby, config, "queue_depth")?;
|
|
52
52
|
let queue_timeout_ms: u64 = cfg(ruby, config, "queue_timeout_ms")?;
|
|
53
53
|
let request_timeout_ms: u64 = cfg_opt::<u64>(ruby, config, "request_timeout_ms")?.unwrap_or(0);
|
|
54
|
+
let max_body_size: usize = cfg_opt::<usize>(ruby, config, "max_body_size")?.unwrap_or(0);
|
|
55
|
+
let max_connections: usize = cfg_opt::<usize>(ruby, config, "max_connections")?.unwrap_or(1024);
|
|
54
56
|
let tokio_threads: usize = cfg_opt::<usize>(ruby, config, "tokio_threads")?.unwrap_or(0);
|
|
55
57
|
let tls_cert: Option<String> = cfg_opt(ruby, config, "tls_cert")?;
|
|
56
58
|
let tls_key: Option<String> = cfg_opt(ruby, config, "tls_key")?;
|
|
@@ -104,6 +106,7 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16), Er
|
|
|
104
106
|
rejected: std::sync::atomic::AtomicU64::new(0),
|
|
105
107
|
queue_timeout_ms,
|
|
106
108
|
request_timeout_ms,
|
|
109
|
+
max_body_size,
|
|
107
110
|
timeouts: std::sync::atomic::AtomicU64::new(0),
|
|
108
111
|
https: acceptor.is_some(),
|
|
109
112
|
access_log: log_requests.then(|| crate::logsink::Sink::new(std::io::stdout())),
|
|
@@ -120,6 +123,7 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16), Er
|
|
|
120
123
|
tokio_listener,
|
|
121
124
|
acceptor,
|
|
122
125
|
server.clone(),
|
|
126
|
+
max_connections,
|
|
123
127
|
shutdown_rx,
|
|
124
128
|
));
|
|
125
129
|
*server.runtime.lock() = Some(runtime);
|
|
@@ -129,40 +133,73 @@ pub fn server_start(ruby: &Ruby, config: magnus::RHash) -> Result<(u64, u16), Er
|
|
|
129
133
|
Ok((id, local_port))
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
/// Slowloris guard for TLS: a client that completes the TCP connect but then
|
|
137
|
+
/// stalls the handshake would otherwise hold a connection slot indefinitely
|
|
138
|
+
/// (the per-request and header-read deadlines only start once hyper is
|
|
139
|
+
/// serving, i.e. after the handshake). A handshake is a few round trips, so
|
|
140
|
+
/// this is generous even for a high-latency client. Fixed, like the header
|
|
141
|
+
/// timeout: not a knob.
|
|
142
|
+
const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
|
|
143
|
+
|
|
132
144
|
async fn accept_loop(
|
|
133
145
|
listener: tokio::net::TcpListener,
|
|
134
146
|
acceptor: Option<tokio_rustls::TlsAcceptor>,
|
|
135
147
|
server: Arc<ServerInner>,
|
|
148
|
+
max_connections: usize,
|
|
136
149
|
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
|
|
137
150
|
) {
|
|
151
|
+
// Bound concurrent connections: unbounded, a flood spawns a task and holds
|
|
152
|
+
// a socket per connection until file descriptors or memory run out. One
|
|
153
|
+
// permit per live connection; acquiring BEFORE accept leaves the excess in
|
|
154
|
+
// the kernel backlog (backpressure) rather than accepting then dropping.
|
|
155
|
+
let conn_limit = Arc::new(tokio::sync::Semaphore::new(max_connections));
|
|
138
156
|
loop {
|
|
139
|
-
tokio::select! {
|
|
157
|
+
let permit = tokio::select! {
|
|
140
158
|
_ = shutdown_rx.changed() => break,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
permit = conn_limit.clone().acquire_owned() => match permit {
|
|
160
|
+
Ok(permit) => permit,
|
|
161
|
+
Err(_) => break, // semaphore closed
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
let (stream, remote_addr) = tokio::select! {
|
|
165
|
+
_ = shutdown_rx.changed() => break,
|
|
166
|
+
accepted = listener.accept() => match accepted {
|
|
167
|
+
Ok(pair) => pair,
|
|
168
|
+
Err(_) => continue, // transient accept error; permit drops, retry
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
// Small responses must not wait on Nagle + delayed ACK.
|
|
172
|
+
let _ = stream.set_nodelay(true);
|
|
173
|
+
let local_addr = stream
|
|
174
|
+
.local_addr()
|
|
175
|
+
.unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
|
|
176
|
+
let server = server.clone();
|
|
177
|
+
let acceptor = acceptor.clone();
|
|
178
|
+
tokio::spawn(async move {
|
|
179
|
+
// Held for the connection's lifetime; dropping it frees a slot.
|
|
180
|
+
let _permit = permit;
|
|
181
|
+
match acceptor {
|
|
182
|
+
Some(acceptor) => {
|
|
183
|
+
// Handshake failures (port scans, plain HTTP to a TLS
|
|
184
|
+
// port) and stalled handshakes (slowloris) just drop the
|
|
185
|
+
// connection; the timeout bounds the latter.
|
|
186
|
+
let handshake = tokio::time::timeout(TLS_HANDSHAKE_TIMEOUT, acceptor.accept(stream));
|
|
187
|
+
let Ok(Ok(tls)) = handshake.await else { return };
|
|
188
|
+
serve_connection(tls, server, remote_addr, local_addr).await;
|
|
189
|
+
}
|
|
190
|
+
None => serve_connection(stream, server, remote_addr, local_addr).await,
|
|
161
191
|
}
|
|
162
|
-
}
|
|
192
|
+
});
|
|
163
193
|
}
|
|
164
194
|
}
|
|
165
195
|
|
|
196
|
+
/// Slowloris guard: drop a connection that has not sent its complete request
|
|
197
|
+
/// headers within this window. Long enough never to trip a real client (even
|
|
198
|
+
/// on a slow mobile link), short enough to reap a stalled one. Deliberately a
|
|
199
|
+
/// constant, not a config knob: fine-tuning intake limits is the fronting
|
|
200
|
+
/// proxy's job; the actual hazard was having no default at all.
|
|
201
|
+
const HEADER_READ_TIMEOUT: Duration = Duration::from_secs(15);
|
|
202
|
+
|
|
166
203
|
async fn serve_connection<I>(
|
|
167
204
|
io: I,
|
|
168
205
|
server: Arc<ServerInner>,
|
|
@@ -176,7 +213,13 @@ async fn serve_connection<I>(
|
|
|
176
213
|
// No auto Date header: it costs a clock read per response (together
|
|
177
214
|
// with timer reads, ~7% of tokio-side cycles in the profile); it's a
|
|
178
215
|
// SHOULD not a MUST, and apps that need it can set it themselves.
|
|
216
|
+
//
|
|
217
|
+
// The timer is installed so header_read_timeout actually fires: hyper's
|
|
218
|
+
// slow-header guard is inert without one. It arms only while the request
|
|
219
|
+
// head is being read, so it adds no per-response cost on the hot path.
|
|
179
220
|
let _ = hyper::server::conn::http1::Builder::new()
|
|
221
|
+
.timer(hyper_util::rt::TokioTimer::new())
|
|
222
|
+
.header_read_timeout(HEADER_READ_TIMEOUT)
|
|
180
223
|
.auto_date_header(false)
|
|
181
224
|
.serve_connection(TokioIo::new(io), service)
|
|
182
225
|
.await;
|
|
@@ -198,6 +241,25 @@ fn branded(mut response: HyperResponse) -> HyperResponse {
|
|
|
198
241
|
response
|
|
199
242
|
}
|
|
200
243
|
|
|
244
|
+
/// A single valid Content-Length as a byte count. hyper has already rejected
|
|
245
|
+
/// conflicting/duplicate values, so the first is authoritative; anything
|
|
246
|
+
/// unparseable yields None and the streaming cap still applies.
|
|
247
|
+
fn content_length(headers: &http::HeaderMap) -> Option<u64> {
|
|
248
|
+
headers
|
|
249
|
+
.get(http::header::CONTENT_LENGTH)?
|
|
250
|
+
.to_str()
|
|
251
|
+
.ok()?
|
|
252
|
+
.trim()
|
|
253
|
+
.parse()
|
|
254
|
+
.ok()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// Idle deadline between request-body frames. A client that stalls mid-body
|
|
258
|
+
/// would otherwise hold a worker slot indefinitely (the worker blocks in
|
|
259
|
+
/// read_body). Generous: a real upload sends steadily and resets this each
|
|
260
|
+
/// frame, so only a silent client trips it. Fixed, like the header timeout.
|
|
261
|
+
const BODY_READ_TIMEOUT: Duration = Duration::from_secs(30);
|
|
262
|
+
|
|
201
263
|
async fn handle_request(
|
|
202
264
|
server: Arc<ServerInner>,
|
|
203
265
|
remote_addr: SocketAddr,
|
|
@@ -221,19 +283,50 @@ async fn handle_request(
|
|
|
221
283
|
)
|
|
222
284
|
});
|
|
223
285
|
|
|
286
|
+
// Body-size guard: an honestly-declared oversize body is refused with a
|
|
287
|
+
// 413 below, before any worker runs. Chunked or lying clients are caught
|
|
288
|
+
// by the forwarder, which caps cumulative bytes and flags an overflow so
|
|
289
|
+
// read_body raises instead of letting the app buffer without bound.
|
|
290
|
+
let max_body = server.max_body_size;
|
|
291
|
+
let oversize =
|
|
292
|
+
max_body > 0 && content_length(&parts.headers).is_some_and(|len| len > max_body as u64);
|
|
293
|
+
|
|
224
294
|
// Stream the request body through a bounded channel: hyper is polled
|
|
225
295
|
// only as fast as the Ruby side consumes (inbound backpressure), and the
|
|
226
296
|
// forwarder dropping the sender is EOF. Bodyless requests (most GETs)
|
|
227
297
|
// skip the forwarder task entirely: dropping the sender IS the EOF.
|
|
228
298
|
let (body_tx, body_rx) = flume::bounded::<bytes::Bytes>(8);
|
|
229
|
-
|
|
299
|
+
let body_overflow = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
300
|
+
let body_timeout = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
301
|
+
if oversize || hyper::body::Body::is_end_stream(&body) {
|
|
230
302
|
drop(body_tx);
|
|
231
303
|
} else {
|
|
304
|
+
let overflow = body_overflow.clone();
|
|
305
|
+
let timed_out = body_timeout.clone();
|
|
232
306
|
tokio::spawn(async move {
|
|
233
307
|
let mut body = body;
|
|
234
|
-
|
|
235
|
-
|
|
308
|
+
let mut total: u64 = 0;
|
|
309
|
+
loop {
|
|
310
|
+
// Idle deadline between frames: a client that stalls mid-body
|
|
311
|
+
// would otherwise pin a worker blocked in read_body. Only the
|
|
312
|
+
// client's silence trips this; a slow APP blocks the forwarder
|
|
313
|
+
// in send_async below instead, which is not timed.
|
|
314
|
+
let frame = match tokio::time::timeout(BODY_READ_TIMEOUT, body.frame()).await {
|
|
315
|
+
Ok(Some(Ok(frame))) => frame,
|
|
316
|
+
Ok(Some(Err(_))) | Ok(None) => break, // body error or clean EOF
|
|
317
|
+
Err(_) => {
|
|
318
|
+
timed_out.store(true, Ordering::Relaxed);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
236
322
|
if let Ok(data) = frame.into_data() {
|
|
323
|
+
total += data.len() as u64;
|
|
324
|
+
if max_body > 0 && total > max_body as u64 {
|
|
325
|
+
// Past the cap: flag it and stop pulling. Dropping the
|
|
326
|
+
// sender unblocks read_body, which then raises.
|
|
327
|
+
overflow.store(true, Ordering::Relaxed);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
237
330
|
if body_tx.send_async(data).await.is_err() {
|
|
238
331
|
break; // request handle dropped; stop pulling
|
|
239
332
|
}
|
|
@@ -253,6 +346,8 @@ async fn handle_request(
|
|
|
253
346
|
local_addr,
|
|
254
347
|
https: server.https,
|
|
255
348
|
body_rx,
|
|
349
|
+
body_overflow,
|
|
350
|
+
body_timeout,
|
|
256
351
|
leftover: None,
|
|
257
352
|
slot: None,
|
|
258
353
|
responder,
|
|
@@ -273,6 +368,9 @@ async fn handle_request(
|
|
|
273
368
|
|
|
274
369
|
// Single exit point so the access log sees every outcome, 503s included.
|
|
275
370
|
let response: HyperResponse = 'resp: {
|
|
371
|
+
if oversize {
|
|
372
|
+
break 'resp plain_response(413, "Payload Too Large\n");
|
|
373
|
+
}
|
|
276
374
|
if server.lanes {
|
|
277
375
|
if !dispatch_to_lane(&server, ctx).await {
|
|
278
376
|
break 'resp unavailable(&server);
|
data/lib/kino/configuration.rb
CHANGED
|
@@ -12,11 +12,13 @@ module Kino
|
|
|
12
12
|
bind: "127.0.0.1",
|
|
13
13
|
port: 0,
|
|
14
14
|
workers: nil, # resolved to Etc.nprocessors in #to_h
|
|
15
|
-
threads: 3
|
|
15
|
+
threads: nil, # resolved per mode in Server: 1 in :ractor, 3 in :threaded
|
|
16
16
|
mode: :auto,
|
|
17
17
|
queue_depth: 1024,
|
|
18
|
-
queue_timeout:
|
|
18
|
+
queue_timeout: 5.0,
|
|
19
19
|
request_timeout: nil,
|
|
20
|
+
max_connections: nil, # nil = derive from the open-file limit
|
|
21
|
+
max_body_size: 50 * 1024 * 1024, # 50 MB; nil/0 = unlimited
|
|
20
22
|
batch: 1,
|
|
21
23
|
lanes: false,
|
|
22
24
|
log_requests: false,
|
|
@@ -144,7 +146,8 @@ module Kino
|
|
|
144
146
|
# Worker count (ractors in :ractor mode); defaults to CPU cores.
|
|
145
147
|
def workers(count) = @config.set(:workers, Integer(count))
|
|
146
148
|
|
|
147
|
-
# Threads per worker (I/O concurrency inside one ractor)
|
|
149
|
+
# Threads per worker (I/O concurrency inside one ractor); default is
|
|
150
|
+
# mode-dependent: 1 in :ractor mode, 3 in :threaded.
|
|
148
151
|
def threads(count) = @config.set(:threads, Integer(count))
|
|
149
152
|
|
|
150
153
|
# Dispatch mode: :auto, :ractor, or :threaded.
|
|
@@ -159,6 +162,14 @@ module Kino
|
|
|
159
162
|
# Seconds the app gets before the client receives a 504; nil = off.
|
|
160
163
|
def request_timeout(seconds) = @config.set(:request_timeout, seconds && Float(seconds))
|
|
161
164
|
|
|
165
|
+
# Max connections served at once; beyond it, new connections wait in
|
|
166
|
+
# the kernel backlog. Defaults to most of the open-file limit.
|
|
167
|
+
def max_connections(count) = @config.set(:max_connections, Integer(count))
|
|
168
|
+
|
|
169
|
+
# Max request-body bytes before a 413; nil disables (delegate to a
|
|
170
|
+
# fronting proxy). Default 50 MB.
|
|
171
|
+
def max_body_size(bytes) = @config.set(:max_body_size, bytes && Integer(bytes))
|
|
172
|
+
|
|
162
173
|
# Requests a worker may grab per queue visit (default 1).
|
|
163
174
|
def batch(count) = @config.set(:batch, Integer(count))
|
|
164
175
|
|
data/lib/kino/server.rb
CHANGED
|
@@ -41,11 +41,17 @@ module Kino
|
|
|
41
41
|
@bind = settings[:bind]
|
|
42
42
|
@requested_port = settings[:port]
|
|
43
43
|
@workers = Integer(settings[:workers])
|
|
44
|
-
@threads = Integer(settings[:threads])
|
|
45
44
|
@mode = resolve_mode(settings[:mode])
|
|
45
|
+
# Default threads per mode: 1 in :ractor (threads inside a ractor
|
|
46
|
+
# share its lock; a measured +17% on fast handlers; raise `workers`
|
|
47
|
+
# for I/O concurrency instead), 3 in :threaded (threads ARE the
|
|
48
|
+
# concurrency there).
|
|
49
|
+
@threads = Integer(settings[:threads] || ((@mode == :ractor) ? 1 : 3))
|
|
46
50
|
@queue_depth = Integer(settings[:queue_depth])
|
|
47
51
|
@queue_timeout_ms = (Float(settings[:queue_timeout]) * 1000).round
|
|
48
52
|
@request_timeout_ms = settings[:request_timeout] ? (Float(settings[:request_timeout]) * 1000).round : 0
|
|
53
|
+
@max_connections = settings[:max_connections] ? Integer(settings[:max_connections]) : default_max_connections
|
|
54
|
+
@max_body_size = Integer(settings[:max_body_size] || 0)
|
|
49
55
|
@batch = [Integer(settings[:batch]), 1].max
|
|
50
56
|
@lanes = !!settings[:lanes]
|
|
51
57
|
@log_requests = !!settings[:log_requests]
|
|
@@ -70,6 +76,8 @@ module Kino
|
|
|
70
76
|
bind: @bind, port: @requested_port,
|
|
71
77
|
queue_depth: @queue_depth, queue_timeout_ms: @queue_timeout_ms,
|
|
72
78
|
request_timeout_ms: @request_timeout_ms,
|
|
79
|
+
max_connections: @max_connections,
|
|
80
|
+
max_body_size: @max_body_size,
|
|
73
81
|
tokio_threads: @tokio_threads,
|
|
74
82
|
tls_cert: @tls&.fetch(:cert), tls_key: @tls&.fetch(:key),
|
|
75
83
|
lanes: @lanes, log_requests: @log_requests
|
|
@@ -210,6 +218,18 @@ module Kino
|
|
|
210
218
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
211
219
|
end
|
|
212
220
|
|
|
221
|
+
# Default connection cap: most of the process open-file limit. A
|
|
222
|
+
# connection flood's failure mode is descriptor exhaustion, and in
|
|
223
|
+
# :ractor/:threaded mode the app's own sockets and files share this
|
|
224
|
+
# process's table, so leave headroom. Scales with `ulimit -n`; raise the
|
|
225
|
+
# OS limit (or set max_connections) to allow more.
|
|
226
|
+
def default_max_connections
|
|
227
|
+
soft, = Process.getrlimit(Process::RLIMIT_NOFILE)
|
|
228
|
+
return 65_536 if soft == Process::RLIM_INFINITY
|
|
229
|
+
|
|
230
|
+
[soft * 8 / 10, 64].max
|
|
231
|
+
end
|
|
232
|
+
|
|
213
233
|
def join_workers(deadline)
|
|
214
234
|
if @supervisor
|
|
215
235
|
@supervisor.shutdown([deadline - monotonic_now, 0].max)
|
|
@@ -1,141 +1,121 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
1
|
# Kino configuration.
|
|
4
2
|
# Generated by `kino --init`.
|
|
5
3
|
#
|
|
6
|
-
# Every setting
|
|
7
|
-
#
|
|
8
|
-
#
|
|
4
|
+
# Every setting is shown with its default value and commented out, so
|
|
5
|
+
# this file works as-is: uncomment what you want to change.
|
|
6
|
+
# Command-line flags beat this file; this file beats built-in defaults.
|
|
9
7
|
|
|
10
8
|
## Network
|
|
11
9
|
|
|
12
|
-
# Address to listen on. Use "0.0.0.0" to accept
|
|
10
|
+
# Address to listen on. Use "0.0.0.0" to accept connections from other
|
|
11
|
+
# machines.
|
|
13
12
|
# bind "127.0.0.1"
|
|
14
13
|
|
|
15
|
-
# Port to listen on.
|
|
16
|
-
# The `kino` CLI defaults this to 9292 when nothing else sets it.
|
|
14
|
+
# Port to listen on.
|
|
17
15
|
# port 9292
|
|
18
16
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
17
|
+
# Serve HTTPS. Point these at your certificate and key files (inline
|
|
18
|
+
# PEM strings also work).
|
|
21
19
|
# tls cert: "config/certs/server.pem", key: "config/certs/server.key"
|
|
22
20
|
|
|
23
21
|
## Topology
|
|
24
22
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# multi-core parallelism for Ruby CPU work, one per core is a good start.
|
|
29
|
-
# In :threaded mode the same total (workers × threads) runs as plain
|
|
30
|
-
# Threads on the main ractor.
|
|
31
|
-
|
|
32
|
-
# Defaults to the number of CPU cores (Etc.nprocessors).
|
|
23
|
+
# How many workers to run. Each worker handles requests independently;
|
|
24
|
+
# in :ractor mode every worker runs Ruby in parallel on its own core.
|
|
25
|
+
# Default: one per CPU core.
|
|
33
26
|
# workers 8
|
|
34
27
|
|
|
35
|
-
# Threads
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# threads 3
|
|
28
|
+
# Threads inside each worker. More threads help when your app spends
|
|
29
|
+
# time waiting on databases or other services; they do not make Ruby
|
|
30
|
+
# code run faster. Left unset, Kino picks a sensible default for the
|
|
31
|
+
# mode. If your app waits a lot in :ractor mode, prefer raising
|
|
32
|
+
# `workers` instead.
|
|
33
|
+
# threads 1
|
|
42
34
|
|
|
43
35
|
## Dispatch mode
|
|
44
36
|
#
|
|
45
|
-
# :auto
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# :ractor: require a Ractor-shareable app; raises
|
|
50
|
-
# Kino::UnshareableAppError otherwise. The app must capture
|
|
51
|
-
# nothing mutable: frozen middleware, Ractor.shareable_proc
|
|
52
|
-
# endpoints.
|
|
53
|
-
# :threaded: run ANY Rack app (Rails included) on a classic thread pool.
|
|
37
|
+
# :auto - picks :ractor when your app supports it, else :threaded.
|
|
38
|
+
# :ractor - runs Ruby in parallel on all cores. Your app must be
|
|
39
|
+
# Ractor-shareable; check yours with `kino --check`.
|
|
40
|
+
# :threaded - works with any Rack app, including Rails.
|
|
54
41
|
# mode :auto
|
|
55
42
|
|
|
56
43
|
## Backpressure
|
|
57
44
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
# instead of waiting forever.
|
|
45
|
+
# How many requests may wait in line. When the line stays full, new
|
|
46
|
+
# requests are turned away with a 503 instead of waiting forever.
|
|
61
47
|
# queue_depth 1024
|
|
62
48
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
49
|
+
# How long (in seconds) a request may wait for a free spot before
|
|
50
|
+
# getting the 503.
|
|
51
|
+
# queue_timeout 5.0
|
|
65
52
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# returns, so size this above your slowest legitimate endpoint.
|
|
53
|
+
# Give up on a response after this many seconds: the client gets a 504
|
|
54
|
+
# while your app finishes in the background. Off unless set. Set it
|
|
55
|
+
# above your slowest legitimate endpoint.
|
|
70
56
|
# request_timeout 30
|
|
71
57
|
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
58
|
+
# Most connections to serve at once. Past this, new connections wait in
|
|
59
|
+
# the kernel backlog instead of piling up until the server runs out of
|
|
60
|
+
# file descriptors. Defaults to most of the open-file limit (ulimit -n),
|
|
61
|
+
# so it scales with the OS limit and only bites under a flood.
|
|
62
|
+
# max_connections 8192
|
|
63
|
+
|
|
64
|
+
# Reject request bodies larger than this many bytes with a 413, so an
|
|
65
|
+
# oversized or endless upload can't drive your app to run out of memory.
|
|
66
|
+
# Set to nil to disable and let a fronting proxy handle it. Default: 50 MB.
|
|
67
|
+
# max_body_size 50 * 1024 * 1024
|
|
68
|
+
|
|
69
|
+
# How many requests a worker grabs from the line at once. Leave at 1
|
|
70
|
+
# unless all your endpoints are uniformly fast.
|
|
76
71
|
# batch 1
|
|
77
72
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# fast handlers; semantics under overload are slightly different (per-lane
|
|
81
|
-
# caps with brief dispatcher retries instead of one global queue).
|
|
73
|
+
# Experimental dispatcher that gives each worker its own line. Faster
|
|
74
|
+
# for quick handlers; behavior under heavy overload differs slightly.
|
|
82
75
|
# lanes false
|
|
83
76
|
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
# On color terminals lines are tinted by status class (2xx green,
|
|
88
|
-
# 3xx yellow, 4xx maroon, 5xx bright red). This is the SERVER's view - it
|
|
89
|
-
# includes the 503 rejections your app never sees - and it interleaves
|
|
90
|
-
# cleanly with your app's own log (e.g. Rails') on stdout. See also
|
|
91
|
-
# Kino::Logger for routing the app log through the same async sink.
|
|
92
|
-
#
|
|
93
|
-
# Try enabling it in the development environment.
|
|
77
|
+
# Print one line per request to stdout, colored by status on a
|
|
78
|
+
# terminal. This is the server's view: it includes requests your app
|
|
79
|
+
# never saw, such as 503s. Recommended in development.
|
|
94
80
|
# log_requests false
|
|
95
81
|
|
|
96
82
|
## Lifecycle
|
|
97
83
|
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
# reaped. A second INT/TERM force-exits immediately.
|
|
84
|
+
# On shutdown, give in-flight requests this many seconds to finish.
|
|
85
|
+
# A second Ctrl-C (or signal) force-exits immediately.
|
|
101
86
|
# shutdown_timeout 30
|
|
102
87
|
|
|
103
|
-
# Write the
|
|
88
|
+
# Write the server's process id to this file on start.
|
|
104
89
|
# pidfile "tmp/pids/kino.pid"
|
|
105
90
|
|
|
106
91
|
## Runtime
|
|
107
92
|
|
|
108
|
-
# Threads for the
|
|
109
|
-
#
|
|
110
|
-
# real lever: `tokio_threads 1` + `threads 1` measured +26% on a pure-CPU
|
|
111
|
-
# benchmark (every spare thread is Ruby work you didn't run).
|
|
93
|
+
# Threads for the Rust I/O engine. The default suits most apps; for
|
|
94
|
+
# heavily CPU-bound apps, try 1 to leave more cores for Ruby.
|
|
112
95
|
# tokio_threads 4
|
|
113
96
|
|
|
114
97
|
## App
|
|
115
98
|
|
|
116
|
-
# Rackup file
|
|
99
|
+
# Rackup file to load (a command-line argument wins).
|
|
117
100
|
# rackup "config.ru"
|
|
118
101
|
|
|
119
|
-
# Sets RACK_ENV
|
|
102
|
+
# Sets RACK_ENV before the app is loaded, unless already set.
|
|
120
103
|
# environment "production"
|
|
121
104
|
|
|
122
105
|
## Rails
|
|
123
106
|
#
|
|
124
|
-
# Rails runs on Kino
|
|
107
|
+
# Rails runs on Kino today in :threaded mode:
|
|
125
108
|
#
|
|
126
109
|
# mode :threaded
|
|
127
110
|
# environment "production"
|
|
128
111
|
# threads 5 # match your database pool size
|
|
129
112
|
#
|
|
130
|
-
#
|
|
131
|
-
# -
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
# -
|
|
135
|
-
# - Rails.logger goes to stdout/stderr or a thread-safe device.
|
|
113
|
+
# Rails-side tips:
|
|
114
|
+
# - Run with eager loading and no code reloading (the production
|
|
115
|
+
# defaults).
|
|
116
|
+
# - Set the database pool to at least workers x threads.
|
|
117
|
+
# - Send logs to stdout or another thread-safe destination.
|
|
136
118
|
#
|
|
137
|
-
# Rails
|
|
138
|
-
# Rails.application
|
|
139
|
-
#
|
|
140
|
-
# Ractor.make_shareable(Rails.application) succeeds, `mode :ractor` here
|
|
141
|
-
# is all you'll need to change.
|
|
119
|
+
# Ractor mode cannot run Rails yet; the blockers are upstream in Rails.
|
|
120
|
+
# Once `Ractor.make_shareable(Rails.application)` works, switching to
|
|
121
|
+
# `mode :ractor` here is all you will need.
|
data/lib/kino/version.rb
CHANGED
data/sig/kino.rbs
CHANGED
|
@@ -92,6 +92,8 @@ module Kino
|
|
|
92
92
|
def queue_depth: (int depth) -> untyped
|
|
93
93
|
def queue_timeout: (Numeric seconds) -> untyped
|
|
94
94
|
def request_timeout: (Numeric? seconds) -> untyped
|
|
95
|
+
def max_connections: (int count) -> untyped
|
|
96
|
+
def max_body_size: (int? bytes) -> untyped
|
|
95
97
|
def batch: (int count) -> untyped
|
|
96
98
|
def lanes: (boolish enabled) -> untyped
|
|
97
99
|
def log_requests: (boolish enabled) -> untyped
|