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.
@@ -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
- accepted = listener.accept() => {
142
- let Ok((stream, remote_addr)) = accepted else { continue };
143
- // Small responses must not wait on Nagle + delayed ACK.
144
- let _ = stream.set_nodelay(true);
145
- let local_addr = stream
146
- .local_addr()
147
- .unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0)));
148
- let server = server.clone();
149
- let acceptor = acceptor.clone();
150
- tokio::spawn(async move {
151
- match acceptor {
152
- Some(acceptor) => {
153
- // Handshake failures (port scans, plain HTTP to a
154
- // TLS port) just drop the connection.
155
- let Ok(tls) = acceptor.accept(stream).await else { return };
156
- serve_connection(tls, server, remote_addr, local_addr).await;
157
- }
158
- None => serve_connection(stream, server, remote_addr, local_addr).await,
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
- if hyper::body::Body::is_end_stream(&body) {
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
- while let Some(frame) = body.frame().await {
235
- let Ok(frame) = frame else { break };
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);
@@ -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: 1.0,
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 below is shown with its default value, commented out:
7
- # the file is a valid no-op until you uncomment something. Precedence:
8
- # explicit Server.new kwargs / CLI flags > this file > built-in defaults.
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 non-local connections.
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. 0 picks an ephemeral port (readable via server.port).
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
- # TLS termination (rustls, in Rust; never blocks a Ruby thread).
20
- # Values are file paths or inline PEM strings. ALPN is http/1.1.
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
- # Puma-style two-level topology: `workers` × `threads`.
26
- #
27
- # In :ractor mode, `workers` is the number of worker Ractors: true
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 per worker. Threads inside one ractor share its lock, so they
36
- # only add concurrency where handlers block on I/O (database calls, HTTP).
37
- # CPU-bound apps gain nothing past 1 (and pay a lock-handoff tax: threads 1
38
- # measured +17% on fast handlers). I/O-heavy apps want more SLOTS overall -
39
- # in :ractor mode prefer raising `workers` over `threads` (slots are cheap,
40
- # no fork memory): 32 workers x 1 thread beat 8x3 by +35% on waits.
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: :ractor when the app is Ractor-shareable, else :threaded
46
- # (with a warning). Note: a Class used as a Rack app always
47
- # counts as "shareable" even if calling it touches unshareable
48
- # state; force :threaded for those.
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
- # Bounded request queue between the Rust front-end and Ruby workers.
59
- # When it stays full past queue_timeout, clients get an immediate 503
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
- # Seconds a request may wait for queue space before the 503.
64
- # queue_timeout 1.0
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
- # Seconds the app gets to produce a response before the client receives a
67
- # 504 instead. Off by default (nil = wait forever). The handler is NOT
68
- # killed - its late response is dropped and its slot stays busy until it
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
- # Requests a worker may grab per queue visit. Values above 1 squeeze more
73
- # throughput out of uniformly fast handlers, but add head-of-line blocking
74
- # behind slow ones and stretch the effective queue depth - leave at 1
75
- # unless your handlers are all sub-millisecond.
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
- # EXPERIMENTAL lane dispatch: per-worker queues with awake-preferring
79
- # assignment and work stealing. Cuts per-request wakeups for uniformly
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
- # Native access log: one line per request to stdout, written by a
85
- # Rust-side flusher thread - request threads never block on the log.
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
- # Graceful-shutdown drain deadline in seconds: in-flight requests get this
99
- # long to finish; past it, their clients receive 500s and workers are
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 master PID here on start; removed on graceful shutdown.
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 tokio (Rust I/O) runtime. Default (nil) lets tokio use
109
- # one per core: right for I/O-heavy apps. For CPU-heavy apps this is a
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 the `kino` CLI loads (positional CLI argument wins).
99
+ # Rackup file to load (a command-line argument wins).
117
100
  # rackup "config.ru"
118
101
 
119
- # Sets RACK_ENV (unless already set) before the app is loaded by the CLI.
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 TODAY in :threaded mode; uncomment for a Rails app:
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
- # Recommended Rails-side settings to pair with Kino:
131
- # - config.eager_load = true and no code reloading (production defaults):
132
- # Kino's workers serve concurrently; lazy class loading under
133
- # concurrency is slow and, in ractor mode, unsafe.
134
- # - Database pool >= workers × threads (config/database.yml `pool:`).
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 main is being ractorized, but
138
- # Rails.application still captures unshareable state at boot; known
139
- # blockers are documented in Kino's README. Track rails/rails main; when
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Kino
4
4
  # The gem version (single source of truth; ext/kino/Cargo.toml syncs).
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.2"
6
6
  end
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
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.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Markin