kino 0.4.0-aarch64-linux → 0.6.0-aarch64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 145273ae455b0822a24794c25d449376b3ba5122f3f275c00b4a959fdafd5183
4
- data.tar.gz: c20ae1c2f111a4f6329ea25a4d9c9a0579dfa1b77765ff11e35572f8e32925ca
3
+ metadata.gz: 47797b5264a1cdd29fcccc4e370cbf8e92fc71b46daaf3831e7bca69c51affea
4
+ data.tar.gz: 98b264a8a72224f37136e4ec14154e35c1b378844bbe73f35c6110dee59ffde9
5
5
  SHA512:
6
- metadata.gz: ed2ef3b3d79817b67da62e8271f0a420d4ee0c8976780d97e6e91ba15cddff2e1d34e830d41a8c4c8c63d65fecbeab32bec8d8568777b399c684cdfce00624f0
7
- data.tar.gz: 6d695d16998c2bdb9fe44ac709a311990c53c88ae980f136757db926ba370e5a8e379fdd098f675beae4fb126a0ce7d663b36b0a602d6933b1c2b6d3f6cdc02b
6
+ metadata.gz: 9035020f4e0f2a47666391c05c8a7992176258f186ccc0613fb97de3609bdd98866ab015bd1263246245dc9be0622dfbd93880516bca2d818f5d5bf2ea65739c
7
+ data.tar.gz: fd323d0e09a40124a91b19f1142f335c41f2e97fc8d33369fc437e5a1cb50fcf91bd82aeb74f85d3c6f577533e1345f1cf323b2004784a6ab21df9d7b5839648
data/CHANGELOG.md CHANGED
@@ -1,3 +1,59 @@
1
+ ## [0.6.0] - 2026-09-01
2
+
3
+ - HTTP/2 support. Kino now speaks HTTP/2 natively, on by default:
4
+ browsers negotiate it over TLS (ALPN), h2-preferring balancers and
5
+ tools get prior-knowledge h2c on plaintext, and everyone else keeps
6
+ getting HTTP/1.1 on the same port. `http2 false` restores an
7
+ HTTP/1-only server. What ships with it:
8
+ - A spec-complete Rack env on h2: `SERVER_PROTOCOL` is `"HTTP/2"`,
9
+ `HTTP_HOST`/`SERVER_NAME`/`SERVER_PORT` come from the `:authority`
10
+ pseudo-header, split cookie headers are rejoined, and uploads
11
+ stream with the same backpressure as HTTP/1.
12
+ - Uploads at full speed: body reads now drain every arrived chunk in
13
+ one native call, so h2's 16 KB data frames don't pay a GVL
14
+ round-trip each—h2 uploads run at HTTP/1 parity, and chunked
15
+ HTTP/1 uploads got faster too.
16
+ - `MAX_CONCURRENT_STREAMS` advertised from worker-slot capacity, so
17
+ an h2-aware balancer sees the server's real admission and a single
18
+ connection cannot multiply into hundreds of queued requests.
19
+ - Graceful connection drains: on shutdown, every connection finishes
20
+ its in-flight request and then closes (`Connection: close` on
21
+ HTTP/1, GOAWAY on h2) instead of being cut mid-stream at the
22
+ deadline.
23
+ - Interned header values: low-cardinality headers (user-agent,
24
+ accept-*, sec-ch-*) reuse one frozen string instead of allocating
25
+ per request—the env-side analogue of HPACK's wire dedup, worth a
26
+ few percent on header-heavy traffic and 6-8 fewer allocations per
27
+ request, on HTTP/1 too. Cookies and authorization are deliberately
28
+ never cached.
29
+
30
+ Measured on the reference box: native h2 is +79% over HTTP/1.1
31
+ cleartext and 2× over TLS on the same server, +50% over
32
+ nginx-terminated h2 in front of Kino, ~3× falcon on fast handlers.
33
+ - Leaner request hot path: up to 2-4% more throughput on fast handlers
34
+ with `lanes` (measured on Linux), no change elsewhere.
35
+ - All benchmarks re-measured from scratch on a fresh reference box
36
+ (c7a.2xlarge, Ruby 4.0.6, Puma 8.0.2): the 2026-06 numbers
37
+ reproduced within 1-3% at matched configurations, memory to the
38
+ megabyte. The published tables carry the new run, with two additions
39
+ and one honest downgrade: new studies for sharded I/O (+1-3% on 8
40
+ cores, best at `io_threads 8`) and HTTP/2 (above), and a narrower
41
+ ractor /cpu lead over the cluster (+25%, was +34%) because Puma
42
+ 8.0.2 itself got faster. The bench harness gained the sharded-I/O
43
+ lanes and the Rails memory comparison, and the methodology notes now
44
+ record two measurement traps: memory is only comparable after the
45
+ full endpoint battery, and /io only at equal slot counts.
46
+
47
+ ## [0.5.0] - 2026-08-29
48
+
49
+ - Add opt-in `io_shards true`: accepted HTTP connections can run on
50
+ current-thread Tokio I/O shards instead of Tokio's shared multi-thread
51
+ runtime, reducing scheduler contention on very fast handlers. `io_threads`
52
+ sets the shard count; otherwise Kino uses half the available CPUs
53
+ ([Patrik Wenger](https://github.com/paddor)).
54
+ - Update Rust dependencies: hyper 1.11.1, rb-sys 0.9.130,
55
+ rustls-webpki 0.103.15, among others.
56
+
1
57
  ## [0.4.0] - 2026-08-22
2
58
 
3
59
  - Rack handler: `rails server -u kino` and `rackup -s kino` boot Kino
data/README.md CHANGED
@@ -11,11 +11,14 @@ on every core in **one small process**. A **Rust** (tokio + hyper)
11
11
  front-end owns the network, parallel **Ractors** run your Rack 3 app,
12
12
  and a threaded fallback mode runs everything else, Rails included.
13
13
 
14
- * **Fast.** On a real 8-core server, every Kino mode is **1.5-2×**
15
- ahead of a Puma fork cluster on I/O-light endpoints. Ractor mode also
16
- wins on pure CPU, **30%+**. [Benchmarks](#benchmarks) below.
17
- * **A fraction of the memory.** About **~7×** on the simplistic bench
18
- Ractor app, and about **4× less memory** than a Puma cluster serving Rails in fallback threaded mode.
14
+ * **Fast.** On a real 8-core server, every Kino mode is **1.5-1.7×**
15
+ ahead of a Puma fork cluster on I/O-light endpoints—with native
16
+ **HTTP/2** adding another **+79%** over HTTP/1.1 on the same server.
17
+ Ractor mode also wins on pure CPU, **+25%**.
18
+ [Benchmarks](#benchmarks) below.
19
+ * **A fraction of the memory.** About **~8×** on the simplistic bench
20
+ Ractor app, and about **4× less memory** than a Puma cluster serving
21
+ Rails in fallback threaded mode.
19
22
  * **Parallel without forking.** Ractor mode runs CPU work **more than
20
23
  5× faster** than Kino's own GVL-bound threaded mode, in the same
21
24
  small process.
@@ -73,32 +76,32 @@ rather than an app. It is Ractor-shareable, so Kino runs it in `:ractor`
73
76
  mode (and `:threaded` for comparison). **A real Rails app is a different
74
77
  story:** it is *not* Ractor-shareable, so it runs only in Kino's
75
78
  `:threaded` fallback, with its own numbers—see [Rails](#rails) below.
76
- Ruby 4.0.5 with YJIT, every server at its defaults: Puma forks 8 workers ×
77
- 3 threads, Kino stays in one process (8 workers; 1 thread each in ractor
78
- modes, 3 in threaded). Numbers are req/s by wrk (8-second windows, 64
79
- connections, same host). Methodology:
80
- [doc/benchmarks.md](doc/benchmarks.md).
79
+ Ruby 4.0.6 with YJIT (re-measured 2026-09), every server at its
80
+ defaults: Puma forks 8 workers × 3 threads, Kino stays in one process
81
+ (8 workers; 1 thread each in ractor modes, 3 in threaded). Numbers are
82
+ req/s by wrk (8-second windows, 64 connections, same host).
83
+ Methodology: [doc/benchmarks.md](doc/benchmarks.md).
81
84
 
82
85
  | endpoint | Kino :ractor | + lanes | :ractor, `workers 32`² | Kino :threaded | Puma (cluster) |
83
86
  |-------------|-------------:|--------:|-----------------------:|---------------:|---------------:|
84
- | /plaintext | 229,534 | **250,222** | 182,997 | 216,994 | 118,176 |
85
- | /10k | 178,083 | **189,862** | 151,034 | 160,400 | 106,768 |
86
- | /cpu (fib) | **77,999**¹| 70,885 | 66,100 | 13,429 | 58,006 |
87
- | /io (5 ms) | 1,552 | 1,551 | **5,888** | 4,709 | 4,693 |
88
- | /io_native | 1,570 | 1,571 | **6,274** | 4,695 | 4,691 |
87
+ | /plaintext | 222,980 | **244,652** | 115,553 | 213,104 | 142,094 |
88
+ | /10k | 175,182 | **188,365** | 103,147 | 132,009 | 125,116 |
89
+ | /cpu (fib) | **76,695**¹| 71,830 | 54,062 | 13,463 | 61,329 |
90
+ | /io (5 ms) | 1,548 | 1,556 | **5,388** | 4,722 | 4,699 |
91
+ | /io_native | 1,570 | 1,573 | **6,207** | 4,695 | 4,693 |
89
92
 
90
93
  Memory tells two different stories depending on the app, both by **PSS**
91
94
  (proportional set size; see note) after sustained load.
92
95
 
93
96
  **The tiny benchmark app** (Ractor-shareable, so Kino runs it in `:ractor`
94
- or `:threaded`). Kino is **~7× lighter in :ractor mode, ~10× in :threaded**
95
- than the Puma cluster the gap stays large because a trivial app is almost
97
+ or `:threaded`). Kino is **~8× lighter in :ractor mode, ~10× in :threaded**
98
+ than the Puma cluster—the gap stays large because a trivial app is almost
96
99
  all private per-worker heap, which copy-on-write can't share:
97
100
 
98
101
  | tiny app, Kino | Kino (one process) | Puma cluster (8 workers) | ratio |
99
102
  |-----------------|-------------------:|-------------------------:|------:|
100
- | :ractor (8×1) | **148 MB** | 1,068 MB | ~7× |
101
- | :threaded (8×3) | **107 MB**³| 1,068 MB | ~10× |
103
+ | :ractor (8×1) | **135 MB** | 1,072 MB | ~8× |
104
+ | :threaded (8×3) | **107 MB**³| 1,072 MB | ~10× |
102
105
 
103
106
  **A real Rails app** (not Ractor-shareable—Kino's `:threaded` fallback
104
107
  only, [below](#rails)). The gap is **~4×**, smaller because Rails' large
@@ -106,24 +109,25 @@ framework *is* shared copy-on-write across Puma's forks:
106
109
 
107
110
  | Rails hello-world | Kino :threaded | Puma cluster (8 workers) | ratio |
108
111
  |-------------------|---------------:|-------------------------:|------:|
109
- | **PSS** | **92 MB** | **389 MB** | ~4× |
112
+ | **PSS** | **95 MB** | **405 MB** | ~4× |
110
113
 
111
114
  "+ lanes" is the experimental per-worker-queue dispatcher (`lanes true`).
112
115
  It posts the fastest plaintext/10k of any configuration here. Details:
113
116
  [doc/benchmarks.md](doc/benchmarks.md#lane-dispatch-experimental-lanes-true).
114
117
 
115
118
  ¹ Stock settings, no tuning. Ractor mode beats the fork cluster on pure
116
- CPU by +34% (+22% with lanes). Threaded mode shows the GVL ceiling that
117
- every single-process Ruby server hits. The old CPU-tuning recipe is
118
- retired: its `threads 1` half **is** the default now, and its
119
- `tokio_threads 1` half costs −12% on real hardware; see
119
+ CPU by +25% (+17% with lanes; Puma 8.0.2 narrowed this from the +34%
120
+ measured against 7.x). Threaded mode shows the GVL ceiling that every
121
+ single-process Ruby server hits. The old CPU-tuning recipe stays
122
+ retired: its `threads 1` half **is** the default, and its
123
+ `tokio_threads 1` half still costs on real hardware; see
120
124
  [doc/benchmarks.md](doc/benchmarks.md#cpu-bound-tuning).
121
125
 
122
126
  ² Wait-bound throughput is slots ÷ wait, and the default columns bring
123
127
  8 single-thread workers against the cluster's 24 threads. Kino slots
124
128
  are threads, not processes—when your app waits a lot, raise `workers`.
125
- The `workers 32` column is that tuning: **+25% over the cluster on /io
126
- (+34% via `Kino.sleep`)** while still ahead of it on pure CPU, all in
129
+ The `workers 32` column is that tuning: **+15% over the cluster on /io
130
+ (+32% via `Kino.sleep`)** while still ahead of it on pure CPU, all in
127
131
  one small process. The cost is the CPU-light rows (32 ractors
128
132
  oversubscribe 8 cores); pick the topology your app's wait profile
129
133
  needs. See
@@ -131,7 +135,7 @@ needs. See
131
135
 
132
136
  ³ With `MALLOC_ARENA_MAX=2` (the standard Ruby deployment setting;
133
137
  Heroku's default). Without it, 24 threads churning 10 KB responses
134
- through one glibc heap balloon to ~670 MB—an arena-fragmentation
138
+ through one glibc heap balloon to ~647 MB—an arena-fragmentation
135
139
  footgun, not a leak, and ractor mode sidesteps it. See
136
140
  [doc/benchmarks.md](doc/benchmarks.md#memory-under-load-and-the-glibc-arena-footgun).
137
141
 
@@ -141,36 +145,64 @@ doc):
141
145
 
142
146
  | endpoint | Kino :ractor (8×3) | Puma + ractor wrapper | Falcon + ractor wrapper |
143
147
  |------------|-------------------:|----------------------:|------------------------:|
144
- | /plaintext | **193,826** | 19,480 | 99,776 |
145
- | /cpu (fib) | **68,061** | 17,755 | 48,721 |
146
- | /io (5 ms) | **4,530** | 1,454 | 1,549 |
148
+ | /plaintext | **190,206** | 22,055 | 107,203 |
149
+ | /cpu (fib) | **68,230** | 16,528 | 50,101 |
150
+ | /io (5 ms) | **4,477** | 1,482 | 1,545 |
151
+
152
+ (The Kino column here runs 8×3—the same 24 slots the wrappers get—so
153
+ the /io row is comparable; the main table's ractor column runs the 8×1
154
+ default.)
155
+
156
+ ### HTTP/2 head-to-head
157
+
158
+ Kino speaks HTTP/2 natively, so it skips the usual nginx-termination
159
+ hop. Same box as the tables above; all over TLS, h2load, 64 in-flight,
160
+ same app:
161
+
162
+ | /plaintext | req/s | /upload (64 KB) |
163
+ |-------------------------------|------------:|----------------:|
164
+ | Kino native h2 | **164,044** | **19,226** |
165
+ | Kino HTTP/1.1 (same boot) | 80,789 | 17,210 |
166
+ | nginx h2 → Kino HTTP/1.1 | 109,219 | 1,217¹ |
167
+ | Falcon (native h2) | 55,637 | 18,838 |
168
+
169
+ Native h2 **doubles** HTTP/1.1 throughput on the same TLS server
170
+ (+79% on cleartext h2c; fewer, larger socket operations, and HPACK
171
+ spares re-sending cookies) and is **+50% over fronting the same Kino
172
+ with nginx**—the proxy hop is pure cost. Uploads, h2's classic weak
173
+ spot, run at HTTP/1 parity. Full matrix, cleartext h2c lanes, and
174
+ methodology: [doc/benchmarks.md](doc/benchmarks.md#http2); reproduce
175
+ with `bench/h2.sh`.
176
+
177
+ ¹ nginx default-config h2 request-body flow control; see the doc.
147
178
 
148
179
  ### Rails
149
180
 
150
181
  Rails is not Ractor-shareable today, so Kino serves it in `:threaded`
151
- fallback one GVL-bound process. On the same box (`examples/rails-hello`,
182
+ fallback—one GVL-bound process. On the same box (`examples/rails-hello`,
152
183
  edge Rails, production, 8×5):
153
184
 
154
185
  | Rails hello-world | req/s | memory (PSS) |
155
186
  |------------------------------|-------:|-------------:|
156
- | Kino :threaded (one process) | 2,637 | **92 MB** |
157
- | Puma cluster (8 workers) | 12,138 | 389 MB |
187
+ | Kino :threaded (one process) | 2,731 | **95 MB** |
188
+ | Puma cluster (8 workers) | 15,422 | 405 MB |
158
189
 
159
190
  The honest trade-off: Puma's fork cluster uses all 8 cores, so it serves
160
- ~4.6× the throughput at ~4× the memory. Ractor-mode Rails would close
191
+ ~5.6× the throughput—at ~4× the memory. Ractor-mode Rails would close
161
192
  the throughput gap at one-process memory cost; the upstream blockers are
162
193
  tracked in [doc/rails-on-ractors.md](doc/rails-on-ractors.md).
163
194
 
164
- In short: on the tiny synthetic app, ractor mode beats fork-level CPU parallelism (**5.8×** Kino's
165
- own GVL-bound threaded mode, +34% over the cluster) in one process, at
166
- about 1/7th of the cluster's memory by PSS (~4× on a real Rails app).
167
- Every Kino mode is 1.5-2.1× ahead of the cluster on I/O-light endpoints. The macOS numbers
195
+ In short: on the tiny synthetic app, ractor mode beats fork-level CPU parallelism (**5.7×** Kino's
196
+ own GVL-bound threaded mode, +25% over the cluster) in one process, at
197
+ about 1/8th of the cluster's memory by PSS (~4× on a real Rails app).
198
+ Every Kino mode is 1.5-1.7× ahead of the cluster on I/O-light endpoints,
199
+ and native HTTP/2 adds +79% over HTTP/1.1 on top. The macOS numbers
168
200
  (secondary; everything there hits the loopback ceiling) and the
169
201
  YJIT × Ractors gotcha are in [doc/benchmarks.md](doc/benchmarks.md).
170
202
 
171
203
  Reproduce: `bench/run.sh [seconds] [concurrency]` for the main table,
172
204
  `bench/studies.sh` for the follow-ups (CPU recipe, topology, scaling,
173
- logging, memory).
205
+ sharded I/O, logging, memory), `bench/h2.sh` for the HTTP/2 matrix.
174
206
 
175
207
  ## Install
176
208
 
@@ -239,6 +271,7 @@ server = Kino::Server.new(app,
239
271
  control_bind: "127.0.0.1:9293", # monitoring: /stats /metrics /ready /live; port 0 reads back via server.control_port
240
272
  control_token: ENV["KINO_CONTROL_TOKEN"], # optional Bearer auth for /stats + /metrics
241
273
  tls: { cert: "cert.pem", key: "key.pem" }, # file paths or inline PEM
274
+ http2: true, # ALPN h2 on TLS + plaintext h2c; false = HTTP/1 only
242
275
  )
243
276
  server.start
244
277
  server.shutdown # graceful: drain → deadline → abort stragglers
@@ -259,6 +292,45 @@ server.shutdown # graceful: drain → deadline → abort straggler
259
292
  always counts as "shareable" (classes are), even if calling it touches
260
293
  unshareable state. Force `:threaded` for those.
261
294
 
295
+ ### Sharded I/O
296
+
297
+ `io_shards true` (off by default) moves HTTP I/O from Tokio's shared
298
+ multi-thread runtime onto current-thread shards: one thread accepts and
299
+ hands each connection to the least-loaded shard, which then owns it for
300
+ its lifetime—no work-stealing, no cross-thread wakeups on the hot path.
301
+ On the 8-core reference box fast handlers gain +1-3% (best with
302
+ `io_threads 8`); the win grows with scheduler contention, so measure on
303
+ your own core count. Ruby-bound endpoints are unchanged. Orthogonal to
304
+ `mode`: it reshapes the Rust side only.
305
+
306
+ ```ruby
307
+ # kino.rb
308
+ io_shards true
309
+ io_threads 8 # optional; default: half the available CPUs
310
+ ```
311
+
312
+ ### HTTP/2
313
+
314
+ On by default, on both transports, with nothing to configure:
315
+
316
+ - **TLS binds** advertise `h2` via ALPN, so browsers and h2-capable
317
+ clients negotiate HTTP/2 and everything else stays on HTTP/1.1.
318
+ - **Plaintext binds** serve prior-knowledge h2c: a client that opens
319
+ with the HTTP/2 preface (an h2-preferring load balancer, a gRPC-style
320
+ backend hop, `curl --http2-prior-knowledge`) gets HTTP/2; ordinary
321
+ clients are HTTP/1.1 exactly as before. Browsers never do h2 on
322
+ plaintext, so a certificate-less kino behaves identically for them.
323
+
324
+ The Rack side is spec-complete on h2: `SERVER_PROTOCOL` is `"HTTP/2"`,
325
+ `HTTP_HOST`/`SERVER_NAME`/`SERVER_PORT` come from the `:authority`
326
+ pseudo-header (h2 requests carry no Host header), split cookie headers
327
+ are rejoined with `"; "`, and streamed uploads flow through the same
328
+ backpressured body channel as HTTP/1. Streams multiplex into the same
329
+ worker slots as keep-alive requests—`workers × threads` bounds
330
+ concurrency either way. (`rack.hijack` stays out on h2 as it is on h1;
331
+ the protocol has no 101 upgrade to hijack anyway.) `http2 false` pins
332
+ the server to HTTP/1 and drops `h2` from ALPN.
333
+
262
334
  ## Config file and CLI
263
335
 
264
336
  Settings can live in a Puma-style Ruby DSL file: `kino.rb` in the
@@ -424,8 +496,8 @@ not `queue_time`.
424
496
 
425
497
  ## Logging
426
498
 
427
- With one log line per request, `Kino::Logger` sustained **2.4× the
428
- throughput of a shared `::Logger`** (149k vs 63k req/s on the benchmark
499
+ With one log line per request, `Kino::Logger` sustained **1.7× the
500
+ throughput of a shared `::Logger`** (155k vs 90k req/s on the benchmark
429
501
  box). There are two native pieces. Both write through a lock-free
430
502
  channel to a Rust flusher thread, so request threads never take a log
431
503
  mutex and never make a write syscall:
data/doc/architecture.md CHANGED
@@ -11,10 +11,13 @@ tokio (Rust threads) Ruby
11
11
  └──────────────────────────┘ └────────────────────────────┘
12
12
  ```
13
13
 
14
- All network I/O lives in Rust on a tokio multi-threaded runtime; hyper
15
- parses HTTP/1.1 and handles keep-alive; rustls terminates TLS. Ruby never
16
- touches a socket. Each request becomes a Rust-side `RequestCtx` pushed to a
17
- bounded flume MPMC queue; Ruby workers pull from it.
14
+ All network I/O lives in Rust on tokio runtimes; hyper parses HTTP/1.1 and
15
+ handles keep-alive; rustls terminates TLS. The default is Tokio's
16
+ multi-thread runtime. With `io_shards true`, one current-thread runtime
17
+ accepts connections and assigns them to current-thread I/O shards, where
18
+ each connection stays for its lifetime. Ruby never touches a socket. Each
19
+ request becomes a Rust-side `RequestCtx` pushed to a bounded flume MPMC
20
+ queue; Ruby workers pull from it.
18
21
 
19
22
  ## Topology
20
23
 
@@ -106,13 +109,63 @@ plain decrement after an `.await` would never run).
106
109
 
107
110
  ## Graceful shutdown
108
111
 
109
- `stop_accepting` → drain until queue + in-flight reach zero or the
110
- deadline passes `close_queue` (idle workers see Disconnected and exit) →
112
+ `stop_accepting` → every live connection switches to graceful shutdown
113
+ (finish the in-flight request, answer with `Connection: close` on
114
+ HTTP/1 or GOAWAY on h2, take nothing new—so drains converge instead
115
+ of chasing chatty keep-alive clients) → drain until queue + in-flight
116
+ reach zero or the deadline passes → `close_queue` (idle workers see
117
+ Disconnected and exit) →
111
118
  join workers → past deadline: abort remaining clients (a 500, or a
112
119
  connection abort mid-stream), interrupt blocked workers, reap
113
120
  stragglers → tear down the tokio runtime. Idempotent;
114
121
  a second INT/TERM force-exits.
115
122
 
123
+ ## HTTP/2
124
+
125
+ One connection builder serves every protocol: hyper-util's auto builder
126
+ picks h2 by ALPN on TLS connections, by the 24-byte preface sniff on
127
+ plaintext (prior-knowledge h2c), and HTTP/1.x otherwise; `http2 false`
128
+ pins the HTTP/1 codec and skips the sniff entirely. Streams multiplex
129
+ into the same bounded queue as keep-alive requests, so h2 concurrency
130
+ is bounded by workers × threads exactly as h1's is, and the bounded
131
+ body channels give per-stream backpressure for free: while the
132
+ forwarder blocks, hyper withholds WINDOW_UPDATE and the client stalls.
133
+
134
+ The env bridge fills SERVER_NAME/SERVER_PORT/HTTP_HOST from the request
135
+ URI's `:authority` (h2 requests carry no Host header), through the same
136
+ host LRU the Host-header path uses, keyed by the authority bytes;
137
+ SERVER_PROTOCOL is the interned "HTTP/2". h2 trailer frames are dropped
138
+ (Rack has no trailer surface), and the h2 codec itself rejects
139
+ connection-ish headers before they can reach the env.
140
+
141
+ The upload path needed one h2-shaped fix: `read_body` drains every
142
+ already-queued chunk in a single native call (one GVL round-trip and
143
+ one Ruby string per 64 KB read), because a body arriving as 16 KB DATA
144
+ frames otherwise paid one crossing per frame—that alone took h2
145
+ uploads from half of h1's throughput to parity. A knob sweep over
146
+ hyper's h2 codec (frame size, adaptive windows, window sizes) moved
147
+ nothing after that, so hyper's defaults stay.
148
+
149
+ SETTINGS_MAX_CONCURRENT_STREAMS is derived from slot capacity
150
+ (workers × threads, clamped to [8, 1024]) rather than hyper's flat 200:
151
+ a smart balancer sees the server's true admission, and a hostile client
152
+ cannot multiply one connection into hundreds of queued requests—the
153
+ h2 analogue of h1's one-request-per-connection shape. The codec's own
154
+ abuse bounds ship as hyper/h2 defaults and were reviewed: 16 KB header
155
+ lists, 20 pending remote resets then GOAWAY (rapid reset), per-second
156
+ reset-churn and empty-frame budgets.
157
+
158
+ Low-cardinality header values (UA, accept-*, sec-ch-*, sec-fetch-*)
159
+ are interned in an LRU of frozen strings—the env-side analogue of
160
+ HPACK's wire dedup, and the practical form of it: hyper does not expose
161
+ HPACK table indices, so the cache keys on value bytes and works for
162
+ HTTP/1 too. Cookie and authorization are deliberately excluded
163
+ (per-user cardinality, secret lifetime).
164
+
165
+ Deferred until benchmarks justify it: slot-aware flow-control window
166
+ grants (a memory/abuse lever, not a throughput one—the knob sweep
167
+ showed windows don't gate upload throughput).
168
+
116
169
  ## Timer waits: `Kino.sleep`
117
170
 
118
171
  MRI's `sleep` parks the thread on the VM timer, whose wakeups inside
@@ -125,7 +178,8 @@ at the interrupt tick so `Thread#kill` and shutdown stay responsive.
125
178
 
126
179
  - **tokio + hyper**: the bottleneck is the Ruby dispatch boundary, not raw
127
180
  I/O throughput; what matters is HTTP correctness, keep-alive, TLS, and
128
- h2-later—hyper's territory. Cross-platform out of the box.
181
+ h2 (since shipped, via hyper-util's protocol-auto builder)—hyper's
182
+ territory. Cross-platform out of the box.
129
183
  - **monoio**: thread-per-core io_uring looks great in echo-server
130
184
  benchmarks, but hyper only works through its poll-io compat layer
131
185
  (forfeiting io_uring on the hot path), and the share-nothing advantage
data/doc/benchmarks.md CHANGED
@@ -17,9 +17,21 @@ the deployment most apps run today.
17
17
  9R14 (Genoa), 16 GB RAM, Amazon Linux 2023, kernel 6.18. A realistic
18
18
  app-server size, deliberately: nobody provisions a 32-core box per
19
19
  app process.
20
- - Toolchain built on the box via mise: Ruby 4.0.5 (**YJIT enabled**,
21
- `RUBY_YJIT_ENABLE=1` for every server), Rust 1.96, Kino compiled in
20
+ - Toolchain built on the box via mise: Ruby 4.0.6 (**YJIT enabled**,
21
+ `RUBY_YJIT_ENABLE=1` for every server), Rust stable, Kino compiled in
22
22
  the release profile.
23
+ - **2026-09 full re-measurement** (a fresh c7a.2xlarge): every number in
24
+ this document and the README was re-run on Ruby 4.0.6 / kernel 6.18 /
25
+ Puma 8.0.2, adding the sharded-I/O and HTTP/2 studies. Kino's numbers
26
+ reproduced within 1-3% across the board—including the /io slot
27
+ ceiling and the arena balloon, both intact. The one real shift: Puma
28
+ 8.0.2 is faster than 7.x (142k plaintext, 61k /cpu), narrowing
29
+ ractor mode's /cpu lead to +25% (was +34%). Reversed-boot-order
30
+ re-runs reproduced within ~1%. A methodology trap worth recording:
31
+ a 5-second single-endpoint warmup understates memory badly (81 MB
32
+ where the full battery shows 137 MB) and hides the arena balloon
33
+ entirely—memory is only comparable after the full endpoint battery,
34
+ and /io numbers are only comparable at equal slot counts.
23
35
  - Load generator: wrk 4.2 on the same host, 8-second windows, 64
24
36
  connections (`bench/run.sh 8 64`). Same-host load generation costs
25
37
  both sides CPU equally; we verified the generator was not the
@@ -249,14 +261,17 @@ visible.
249
261
 
250
262
  | config | RSS | PSS |
251
263
  |---|---:|---:|
252
- | Kino :ractor 8×1 (default) | 151 | **148** |
253
- | Kino lanes 8×1 | 137 | **135** |
254
- | Kino :ractor 8×3 | 171 | **169** |
264
+ | Kino :ractor 8×1 (default) | 137 | **135** |
265
+ | Kino lanes 8×1 | 128 | **126** |
266
+ | Kino :ractor 8×3 | 170 | **168** |
255
267
  | Kino :threaded 8×3 (`MALLOC_ARENA_MAX=2`) | 109 | **107** |
256
- | Kino :threaded 8×3 (no arena cap) | 668 | **666**¹ |
257
- | Puma cluster 8×3 | 1,213 | **1,068** |
268
+ | Kino :threaded 8×3 (no arena cap) | 672 | **670**¹ |
269
+ | Puma cluster 8×3 | 1,216 | **1,072** |
258
270
 
259
- The tiny app is ~7× lighter than the cluster in ractor mode, ~10× in
271
+ (2026-09 re-measure; the 2026-06 numbers reproduced within a few MB on
272
+ every row—the arena-capped threaded row to the megabyte.)
273
+
274
+ The tiny app is ~8× lighter than the cluster in ractor mode, ~10× in
260
275
  arena-capped threaded mode. RSS ≈ PSS for every Kino row (one process,
261
276
  nothing to share) and within ~12% for Puma here: a trivial app has almost
262
277
  no shared state, so Puma's footprint is ~1,051 MB of *private* per-worker
@@ -280,18 +295,19 @@ Here copy-on-write **does** matter, which is exactly why PSS is mandatory:
280
295
 
281
296
  | config | RSS | PSS |
282
297
  |---|---:|---:|
283
- | Kino :threaded (one process) | 97 | **92** |
284
- | Puma cluster 8×3 (preload) | 794 | **389** |
298
+ | Kino :threaded (one process) | 97 | **95** |
299
+ | Puma cluster 8×5 (preload) | 813 | **405** |
300
+ | Puma cluster 8×5 (no preload) | 824 | **646** |
285
301
 
286
302
  Puma serves the same Rails framework from 8 forks that share it
287
- copy-on-write; RSS counts that shared framework once per worker (794 MB),
288
- PSS counts it once (389 MB). The fair ratio is **~4×**, not the ~8× a
303
+ copy-on-write; RSS counts that shared framework once per worker (813 MB),
304
+ PSS counts it once (405 MB). The fair ratio is **~4×**, not the ~8× a
289
305
  naive RSS sum reports—this is the correction that prompted the whole
290
- re-measure. Preload barely helps (389 vs 400 MB without): Ruby's GC
291
- dirties most heap pages, breaking copy-on-write, so even a preloaded
292
- cluster keeps a large private heap per worker. That is why "CoW should
293
- make a fork cluster nearly free" is only half true—it shares the code,
294
- not the live object heap.
306
+ re-measure. The 2026-09 run also split out preload: it now saves a
307
+ third (405 vs 646 MB PSS)—worth turning on—but even preloaded, Ruby's
308
+ GC dirties heap pages and breaks copy-on-write, so each worker keeps a
309
+ large private heap. That is why "CoW should make a fork cluster nearly
310
+ free" is only half true—it shares the code, not the live object heap.
295
311
 
296
312
  ## Run-to-run variance (a.k.a. "is this a regression?")
297
313
 
@@ -376,27 +392,46 @@ crash semantics, stealing fairness, and drain behavior have spec
376
392
  coverage but not production mileage. (On loopback-bound macOS, lanes
377
393
  lose a few percent instead; see the secondary table below.)
378
394
 
395
+ ## Sharded I/O (`io_shards true`)
396
+
397
+ `bench/studies.sh 8 64 shards`, ractor 8×3, 2026-09 reference box:
398
+
399
+ | case (/plaintext) | req/s |
400
+ |---|---:|
401
+ | shared tokio runtime (baseline) | 193,461 |
402
+ | io_shards, default shard count | 195,436 |
403
+ | io_shards, `io_threads 2` | 170,509 |
404
+ | io_shards, `io_threads 4` | 196,298 |
405
+ | io_shards, `io_threads 8` | 199,657 |
406
+
407
+ On 8 cores the shards buy +1-3%, best at `io_threads 8` (the
408
+ half-the-cores default is close behind; 2 shards choke on accept
409
+ handoff). The design removes work-stealing and cross-thread wakeups,
410
+ so the win scales with scheduler contention—expect more on boxes with
411
+ more cores and connections, and measure on your own core count.
412
+
379
413
  ## Logging costs
380
414
 
381
415
  Measured at full plaintext saturation (one log line per request—rates
382
416
  that no real deployment logs at; treat these as worst-case ceilings, not
383
417
  typical costs):
384
418
 
385
- | case (8×3, same session) | req/s |
419
+ | case (8×3, same session, 2026-09) | req/s |
386
420
  |---|---:|
387
- | threaded, no logging | 219,168 |
388
- | threaded, `log_requests true` (native access log) | 193,998 (−11%) |
389
- | ractor, access log off / on | 197,596 / 181,050 (−8%) |
390
- | app logs 1 line/req via shared `::Logger` (file) | **62,961** |
391
- | app logs 1 line/req via `Kino::Logger` (file) | **149,519 (2.4×)** |
421
+ | threaded, no logging | 213,166 |
422
+ | threaded, `log_requests true` (native access log) | 173,501 (−19%) |
423
+ | ractor, access log off / on | 191,152 / 163,777 (−14%) |
424
+ | app logs 1 line/req via shared `::Logger` (file) | **90,175** |
425
+ | app logs 1 line/req via `Kino::Logger` (file) | **154,653 (1.7×)** |
392
426
 
393
427
  The shared-`::Logger` cost is the mutex: 24 worker threads serialize
394
428
  through one lock plus a write syscall per line. `Kino::Logger` hands the
395
429
  formatted line to a lock-free channel and returns—the remaining cost vs
396
430
  not logging at all is Ruby-side formatting, which no device can remove.
397
- (In the Docker environment the same comparison showed 8.5×—overlay-fs
398
- write latency punished the synchronous logger far harder than this box's
399
- NVMe does. The ranking is environment-independent; the multiple is not.)
431
+ (The multiple moves with the environment: 2.4× on the 2026-06 box,
432
+ 1.7× on the 2026-09 one, 8.5× under Docker, where overlay-fs write
433
+ latency punished the synchronous logger hardest. The ranking is
434
+ environment-independent; the multiple is not.)
400
435
 
401
436
  One trade-off worth knowing: the sink **never blocks** request threads,
402
437
  so at absurd rates against a slow disk it drops lines once its 8192-line
@@ -408,6 +443,65 @@ Puma comparison note: request logging is opt-in there too (`--quiet` is
408
443
  the default, `-v/--log-requests` enables it)—Kino's default-off
409
444
  `log_requests` matches the ecosystem's standard behavior.
410
445
 
446
+ ## HTTP/2
447
+
448
+ `bench/h2.sh`, Linux only (on macOS run it under Docker; the 2026-09
449
+ numbers below are from the c7a.2xlarge reference box). Every lane is
450
+ measured with h2load so the generator is identical everywhere: h2
451
+ lanes run 8 connections × 8 concurrent streams, h1 lanes 64
452
+ connections—the same total in-flight. Servers without native h2 get
453
+ the standard pattern instead: nginx terminating h2 and proxying
454
+ HTTP/1.1 upstream over keep-alive. One labeled run (kino ractor 8×3,
455
+ falcon `--count 8`, puma `-w 8 -t 3:3`, 5 s/lane); re-run the whole
456
+ script for close calls, per the variance section.
457
+
458
+ | target (h2 unless noted) | /plaintext | /10k | /big-cookie | /upload (64 KB) |
459
+ |---|---:|---:|---:|---:|
460
+ | kino h2c | 207,461 | 150,105 | 195,093 | 26,304 |
461
+ | kino h1 cleartext (same boot) | 116,082 | 97,634 | 110,614 | 24,338 |
462
+ | kino h2 TLS | 164,044 | 116,617 | 154,301 | 19,226 |
463
+ | kino h1 TLS (same boot) | 80,789 | 69,942 | 76,654 | 17,210 |
464
+ | falcon TLS (native h2) | 55,637 | 37,541 | 49,151 | 18,838 |
465
+ | nginx h2 → puma h1 | 81,234 | 57,891 | 51,332 | 1,247 |
466
+ | nginx h2 → kino h1 | 109,219 | 66,634 | 54,996 | 1,217 |
467
+
468
+ What the numbers say:
469
+
470
+ - **Native h2 beats h1 on the same server by +79% cleartext and +103%
471
+ over TLS** on /plaintext: the same 64 in-flight requests ride 8
472
+ connections instead of 64, so frames batch into fewer, larger
473
+ syscalls—and TLS amplifies it, since h1's 64 connections each pay
474
+ crypto per record. The `/big-cookie` lane (a ~2 KB cookie per
475
+ request) shows HPACK on top: the cookie crosses the wire once per
476
+ connection, not once per request, and holds 94% of bare-plaintext
477
+ throughput where h1 loses 5%.
478
+ - **Native h2 beats proxied h2 by +50%** with the *same backend*: the
479
+ nginx→kino-h1 lane is the proxy-cost control, and the extra hop,
480
+ re-parse, and re-serialize cost ~55k req/s on /plaintext.
481
+ - **Uploads run at h1 parity**—but only after a fix this lane
482
+ caught: h2 delivers bodies as 16 KB DATA frames, and `read_body`
483
+ originally crossed the GVL once per chunk, halving upload
484
+ throughput. It now drains every queued chunk per crossing
485
+ (doc/architecture.md), and a knob sweep over hyper's h2 codec
486
+ (frame size, adaptive/bigger windows) moved nothing afterwards.
487
+ nginx's h2 upload collapse (~1.5k) is its default-config
488
+ request-body flow control; tune `http2_body_preread_size`/buffering
489
+ before drawing conclusions there.
490
+ - falcon lands at roughly a third of kino-h2-TLS on fast handlers and
491
+ slightly behind on uploads. Single-run caveat: the nginx lanes
492
+ showed ±20% swings between runs in the Docker environment; on the
493
+ reference box the kino-vs-kino and kino-vs-proxy ratios reproduced
494
+ across runs, the nginx lanes remain the noisiest.
495
+ - **Header-value interning** (user-agent, accept-*, sec-ch-*: one
496
+ frozen string instead of a fresh allocation per request) was
497
+ measured with a realistic 11-header browser set on /plaintext,
498
+ using the within-boot header cost (bare vs with-headers) as the
499
+ drift-resistant metric: over h2 that cost fell from ~16% to ~12-13%
500
+ (~+3-5% throughput on the headers lane); h1's smaller header cost
501
+ stayed within noise. The effect the tiny app understates: 6-8 fewer
502
+ string allocations per request is GC pressure a real app feels more
503
+ than this one does.
504
+
411
505
  ## Hot-path notes
412
506
 
413
507
  For the curious, the dispatch-path work behind the numbers: a try-pop
@@ -26,8 +26,11 @@ 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,
33
+ http2: true,
31
34
  environment: nil,
32
35
  pidfile: nil,
33
36
  control_bind: nil,
@@ -148,6 +151,8 @@ module Kino
148
151
  # queue_depth 2048
149
152
  # queue_timeout 0.5
150
153
  # shutdown_timeout 15
154
+ # io_shards true
155
+ # io_threads 6
151
156
  # tokio_threads 4
152
157
  # tls cert: "cert.pem", key: "key.pem"
153
158
  #
@@ -229,12 +234,23 @@ module Kino
229
234
  # Graceful-shutdown drain deadline in seconds.
230
235
  def shutdown_timeout(seconds) = @config.set(:shutdown_timeout, seconds)
231
236
 
237
+ # Run native HTTP I/O on current-thread shards instead of Tokio's shared pool.
238
+ def io_shards(enabled = true) = @config.set(:io_shards, !!enabled)
239
+
240
+ # Native HTTP I/O shard count; default with io_shards: half available CPUs.
241
+ def io_threads(count) = @config.set(:io_threads, Integer(count))
242
+
232
243
  # Threads for the tokio (Rust I/O) runtime; default: one per core.
233
244
  def tokio_threads(count) = @config.set(:tokio_threads, Integer(count))
234
245
 
235
246
  # TLS termination; file paths or inline PEM strings.
236
247
  def tls(cert:, key:) = @config.set(:tls, {cert: cert, key: key})
237
248
 
249
+ # HTTP/2: negotiated via ALPN on TLS binds, served to
250
+ # prior-knowledge clients on plaintext. On by default; set false
251
+ # to serve HTTP/1 only.
252
+ def http2(enabled = true) = @config.set(:http2, !!enabled)
253
+
238
254
  # Sets RACK_ENV (unless already set) before the CLI loads the app.
239
255
  def environment(env) = @config.set(:environment, env.to_s)
240
256
 
data/lib/kino/kino.so CHANGED
Binary file
data/lib/kino/server.rb CHANGED
@@ -98,11 +98,18 @@ 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?
104
110
  raise ArgumentError, "TLS is not supported on a unix socket bind; terminate TLS at the proxy in front"
105
111
  end
112
+ @http2 = settings.fetch(:http2, true) ? true : false
106
113
  @pidfile = settings[:pidfile]
107
114
  @control_bind = settings[:control_bind]&.to_s
108
115
  @control_token = settings[:control_token]&.to_s
@@ -145,8 +152,11 @@ module Kino
145
152
  request_timeout_ms: @request_timeout_ms,
146
153
  max_connections: @max_connections,
147
154
  max_body_size: @max_body_size,
155
+ io_shards: @io_shards,
156
+ io_threads: @io_threads,
148
157
  tokio_threads: @tokio_threads,
149
158
  tls_cert: @tls&.fetch(:cert), tls_key: @tls&.fetch(:key),
159
+ http2: @http2,
150
160
  lanes: @lanes, log_requests: @log_requests,
151
161
  mode: @mode.to_s, workers: @workers, threads: @threads, batch: @batch,
152
162
  control_bind: @control_bind, control_token: @control_token
@@ -20,6 +20,11 @@
20
20
  # PEM strings also work).
21
21
  # tls cert: "config/certs/server.pem", key: "config/certs/server.key"
22
22
 
23
+ # HTTP/2 is on by default: negotiated via ALPN on TLS binds, and served
24
+ # to prior-knowledge (h2c) clients on plaintext. Everything else keeps
25
+ # getting HTTP/1.1. Set false to serve HTTP/1 only.
26
+ # http2 false
27
+
23
28
  ## Topology
24
29
 
25
30
  # How many workers to run. Each worker handles requests independently;
@@ -125,8 +130,14 @@
125
130
 
126
131
  ## Runtime
127
132
 
128
- # Threads for the Rust I/O engine. The default suits most apps; for
129
- # heavily CPU-bound apps, try 1 to leave more cores for Ruby.
133
+ # Run native HTTP I/O on current-thread shards instead of Tokio's shared
134
+ # worker pool, reducing scheduler contention on very fast handlers.
135
+ # io_shards true
136
+
137
+ # I/O shard count. Default with io_shards: half available CPUs.
138
+ # io_threads 6
139
+
140
+ # Threads for the Tokio multi-thread runtime. Default: one per available CPU.
130
141
  # tokio_threads 4
131
142
 
132
143
  ## Control plane
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.4.0"
5
+ VERSION = "0.6.0"
6
6
  end
data/lib/kino/worker.rb CHANGED
@@ -20,12 +20,16 @@ module Kino
20
20
 
21
21
  def run(server_id, worker_id, app, batch_size = 1, hooks = nil)
22
22
  fire_after_worker_boot(hooks, worker_id)
23
+ # One resolution of (server, worker) into a native handle; every take
24
+ # after this skips the registry lookup. nil = server already gone.
25
+ worker = Native.worker(server_id, worker_id)
26
+ return unless worker
23
27
  if batch_size <= 1
24
- env = Native.take_one(server_id, worker_id)
25
- env = handle_one(env, server_id, worker_id, app, hooks) while env
28
+ env = worker.take_one
29
+ env = handle_one(env, worker, app, hooks) while env
26
30
  else
27
- batch = Native.take_batch(server_id, worker_id, batch_size)
28
- batch = process(batch, server_id, worker_id, app, batch_size, hooks) while batch
31
+ batch = worker.take_batch(batch_size)
32
+ batch = process(batch, worker, app, batch_size, hooks) while batch
29
33
  end
30
34
  end
31
35
 
@@ -35,22 +39,21 @@ module Kino
35
39
  NOT_FUSED = Object.new.freeze
36
40
 
37
41
  # Handle one request; returns the next env (fused take) or nil.
38
- def handle_one(env, server_id, worker_id, app, hooks)
42
+ def handle_one(env, worker, app, hooks)
39
43
  result = serve(env, app, hooks) do |request, status, headers, chunks|
40
- request.respond_and_take_one(server_id, worker_id, status, headers, chunks)
44
+ request.respond_and_take_one(worker, status, headers, chunks)
41
45
  end
42
- result.equal?(NOT_FUSED) ? Native.take_one(server_id, worker_id) : result
46
+ result.equal?(NOT_FUSED) ? worker.take_one : result
43
47
  end
44
48
 
45
49
  # Handle every env in the batch; returns the next batch (the last
46
50
  # simple response rides the fused respond_and_take) or nil on shutdown.
47
- def process(batch, server_id, worker_id, app, batch_size, hooks)
51
+ def process(batch, worker, app, batch_size, hooks)
48
52
  last = batch.size - 1
49
53
  batch.each_with_index do |env, index|
50
54
  result = serve(env, app, hooks) do |request, status, headers, chunks|
51
55
  if index == last
52
- request.respond_and_take(server_id, worker_id, batch_size,
53
- status, headers, chunks)
56
+ request.respond_and_take(worker, batch_size, status, headers, chunks)
54
57
  else
55
58
  request.send_simple(status, headers, chunks)
56
59
  NOT_FUSED
@@ -58,7 +61,7 @@ module Kino
58
61
  end
59
62
  return result if index == last && !result.equal?(NOT_FUSED)
60
63
  end
61
- Native.take_batch(server_id, worker_id, batch_size)
64
+ worker.take_batch(batch_size)
62
65
  end
63
66
 
64
67
  # Run one request through the app. Complete bodies are yielded so the
data/sig/kino.rbs CHANGED
@@ -126,8 +126,11 @@ 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
133
+ def http2: (?boolish enabled) -> untyped
131
134
  def environment: (String | Symbol env) -> untyped
132
135
  def pidfile: (String path) -> untyped
133
136
  def control_bind: (String addr) -> String
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kino
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Yaroslav Markin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-22 00:00:00.000000000 Z
11
+ date: 2026-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '3.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: httpx
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rackup
43
57
  requirement: !ruby/object:Gem::Requirement