kino 0.1.3-aarch64-linux → 0.2.1-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: 1125d6e6dc02e476cd98a406a415cf2ca0921fc95366a043751f3cf0e45344b1
4
- data.tar.gz: 2026c1c8221f96b568d9544a36059079c5dd96af8aa914f07147b93d3bf3ceb5
3
+ metadata.gz: d664928d6dfad3b60c6fdcbd27c2dffc008a9485a224bda3b7179f033c4c7cf5
4
+ data.tar.gz: d52e42477237ff7e65ea5f8752a63c6ac1d1f20cfa3f9189ad3a80a4d5e51f69
5
5
  SHA512:
6
- metadata.gz: bee0236c5585cd08d64bdca886fa7639223d5fe39f5bdcc7abbf1ca4eb9f7fb59e8cf59647641969189c2a3d8331d61472f13f93a9eab7864cd78e9b9895f80d
7
- data.tar.gz: 6e161ab88637c5c1db6ae4ba3cd881850bc171abb08bffee2bc16ec8cf63355d6fee91699891260390e58a014fb8222f5c115e759c8d060acd1cdcef3d85807a
6
+ metadata.gz: e0a0a7ac4cfc422143b11938423cd1b9022e4e065bf131548b485a95d1cd88042798f778b5b87f3fc6a51a12814cf564be8bce8d4ed61366e68c6133e775a3e1
7
+ data.tar.gz: 1d52a5e4715075b324cd9997e525be33c4edf69c4fe0547dcbf8633228783b3f7414699a88a70caeab2d7f1122d80283f4ab7c5cd7f45669bd67b3dae190a12c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [0.2.1] - 2026-07-27
2
+
3
+ - Update Rust dependencies for Kino.
4
+ - Update: puma 8 in benchmarks.
5
+
6
+ ## [0.2.0] - 2026-07-13
7
+
8
+ - Strip debug info from release builds.
9
+ - A panic in the native layer now raises a RuntimeError on the affected
10
+ worker (visible to `on_error` and the error log) instead of killing the
11
+ server process.
12
+ - The pidfile is claimed exclusively: starting refuses (instead of silently
13
+ overwriting) while the pidfile's owner is alive, a leftover file from a
14
+ dead process is replaced, symlinks are never followed, and shutdown
15
+ removes the file only while it still holds our pid.
16
+ - Zero-copy response bodies: bodies of 4 KB and up ride to the network
17
+ layer by reference instead of being copied at the FFI boundary, in both
18
+ dispatch modes. A 10 KB-body endpoint now serves at plaintext speed.
19
+
1
20
  ## [0.1.3] - 2026-07-04
2
21
 
3
22
  - Non-String response header names and values (booleans, numbers, symbols)
data/README.md CHANGED
@@ -21,6 +21,8 @@ and a threaded fallback mode runs everything else, Rails included.
21
21
  small process.
22
22
  * **Production plumbing included.** Graceful drain, crash supervision
23
23
  and respawn, bounded queues with 503 backpressure, request timeouts,
24
+ hardened intake (slowloris and TLS-handshake deadlines, connection
25
+ and body-size caps), an `on_error` hook for your error tracker,
24
26
  TLS (rustls), live stats, async access and app logging.
25
27
  * **Tells you why.** `kino --check` lists exactly what blocks your app
26
28
  from ractor mode, finding by finding, so you do not have to decode
@@ -181,7 +183,7 @@ bundle add kino # or: gem install kino (outside a bundle)
181
183
  or put it in the `Gemfile` yourself:
182
184
 
183
185
  ```ruby
184
- gem "kino", "~> 0.1"
186
+ gem "kino"
185
187
  ```
186
188
 
187
189
  Then generate a config and serve:
@@ -224,6 +226,9 @@ server = Kino::Server.new(app,
224
226
  queue_depth: 1024, # bounded queue; overflow → 503
225
227
  queue_timeout: 5.0, # seconds before 503 on a full queue
226
228
  request_timeout: nil, # seconds before a slow response becomes a 504 (nil = off)
229
+ max_connections: 8192, # cap concurrent connections; default: most of ulimit -n
230
+ max_body_size: 50 * 1024 * 1024, # bytes before a 413; nil = let a proxy handle it
231
+ on_error: ->(e, env) { ErrorTracker.capture(e) }, # after the client got its 500
227
232
  shutdown_timeout: 30, # drain deadline
228
233
  tls: { cert: "cert.pem", key: "key.pem" }, # file paths or inline PEM
229
234
  )
@@ -303,6 +308,18 @@ is unsafe. A stuck handler still occupies its worker slot until it
303
308
  returns, so set the deadline above your slowest legitimate endpoint and
304
309
  watch `stats[:timeouts]`.
305
310
 
311
+ Timeouts guard your app; the network intake guards itself. New
312
+ connections past `max_connections` (default: most of `ulimit -n`) wait
313
+ in the kernel backlog; request bodies past `max_body_size` (default
314
+ 50 MB, `nil` delegates to a fronting proxy) get a **413**; and fixed
315
+ deadlines drop slow-header clients (15 s), stalled TLS handshakes
316
+ (10 s), and uploads stalled mid-body (30 s). When a worker catches an
317
+ app or delivery error,
318
+ `on_error ->(error, env) { ErrorTracker.capture(error) }` is called
319
+ after the client got its 500—the only place a tracker sees errors
320
+ raised while the response was being written (in `:ractor` mode, build
321
+ the handler with `Ractor.shareable_proc`).
322
+
306
323
  ## Stats
307
324
 
308
325
  `server.stats` returns a live snapshot: the configuration plus counters
@@ -415,6 +432,14 @@ bundle exec rake # compile, Rust tests, specs, RBS, lint
415
432
  RB_SYS_CARGO_PROFILE=dev bundle exec rake compile # fast dev rebuilds
416
433
  ```
417
434
 
435
+ ## Acknowledgements
436
+
437
+ Thanks to [Mat Sadler](https://github.com/matsadler) for [magnus](https://github.com/matsadler/magnus).
438
+
439
+ For ractors, thanks to [Koichi Sasada](https://github.com/ko1), [John Hawthorn](https://github.com/jhawthorn), [Jean Boussier](https://github.com/byroot), [Luke Gruber](https://github.com/luke-gruber), and other Ruby core contributors.
440
+
441
+ For the Rust network stack, thanks to [Sean McArthur](https://github.com/seanmonstar) for [hyper](https://github.com/hyperium/hyper), and to [Carl Lerche](https://github.com/carllerche), [Alice Ryhl](https://github.com/Darksonn), and the other [Tokio](https://github.com/tokio-rs/tokio) maintainers for the runtime underneath it. Thanks to [Joshua Barretto](https://github.com/zesterer) for [flume](https://github.com/zesterer/flume)—its channels carry every request between the network side and the workers.
442
+
418
443
  ## Assisted by
419
444
 
420
445
  Claude Code (Mythos, Opus).
data/lib/kino/kino.so CHANGED
Binary file
data/lib/kino/server.rb CHANGED
@@ -73,17 +73,28 @@ module Kino
73
73
  def start
74
74
  raise Error, "server already started" if @started
75
75
 
76
- @id, @port = Native.server_start(
77
- bind: @bind, port: @requested_port,
78
- queue_depth: @queue_depth, queue_timeout_ms: @queue_timeout_ms,
79
- request_timeout_ms: @request_timeout_ms,
80
- max_connections: @max_connections,
81
- max_body_size: @max_body_size,
82
- tokio_threads: @tokio_threads,
83
- tls_cert: @tls&.fetch(:cert), tls_key: @tls&.fetch(:key),
84
- lanes: @lanes, log_requests: @log_requests
85
- )
86
- File.write(@pidfile, "#{Process.pid}\n") if @pidfile
76
+ # Claim the pidfile before binding: refusing to start (another
77
+ # instance is alive) must not leave a booted native runtime behind.
78
+ write_pidfile if @pidfile
79
+ booted = false
80
+ begin
81
+ @id, @port = Native.server_start(
82
+ bind: @bind, port: @requested_port,
83
+ queue_depth: @queue_depth, queue_timeout_ms: @queue_timeout_ms,
84
+ request_timeout_ms: @request_timeout_ms,
85
+ max_connections: @max_connections,
86
+ max_body_size: @max_body_size,
87
+ tokio_threads: @tokio_threads,
88
+ tls_cert: @tls&.fetch(:cert), tls_key: @tls&.fetch(:key),
89
+ lanes: @lanes, log_requests: @log_requests
90
+ )
91
+ booted = true
92
+ ensure
93
+ remove_pidfile if @pidfile && !booted
94
+ end
95
+ # GC anchor for zero-copy response buffers: held for the server's
96
+ # lifetime so in-flight buffers survive even a worker ractor crash.
97
+ @pin_keeper = Native.pin_keeper(@id)
87
98
  if @mode == :ractor
88
99
  @supervisor = RactorSupervisor.new(@id, @app, workers: @workers, threads: @threads,
89
100
  batch: @batch, on_error: @on_error).start
@@ -133,9 +144,12 @@ module Kino
133
144
  end
134
145
 
135
146
  Native.shutdown_runtime(@id, 1_000)
147
+ # The runtime is gone, so hyper has dropped every pinned buffer;
148
+ # the keeper (and the strings it marked) may now be collected.
149
+ @pin_keeper = nil
136
150
  @worker_threads.clear
137
151
  @started = false
138
- File.delete(@pidfile) if @pidfile && File.exist?(@pidfile)
152
+ remove_pidfile if @pidfile
139
153
  nil
140
154
  end
141
155
 
@@ -241,6 +255,67 @@ module Kino
241
255
  [soft * 8 / 10, 64].max
242
256
  end
243
257
 
258
+ # Claim the pidfile for this process. O_EXCL creation fails on ANY
259
+ # existing directory entry (regular file, symlink, even a dangling
260
+ # one), so a live instance's pidfile is never overwritten and a
261
+ # symlink is never followed. A leftover entry whose owner is gone is
262
+ # replaced; one that does not hold a pid is refused, not clobbered.
263
+ def write_pidfile
264
+ claim_pidfile
265
+ rescue Errno::EEXIST
266
+ refuse_unless_stale
267
+ begin
268
+ # Unlink removes the entry itself; a symlink's target is untouched.
269
+ File.unlink(@pidfile)
270
+ rescue Errno::ENOENT
271
+ # Vanished on its own; the claim below settles any remaining race.
272
+ end
273
+ begin
274
+ claim_pidfile
275
+ rescue Errno::EEXIST
276
+ raise Error, "lost the race for #{@pidfile}: another instance is starting"
277
+ end
278
+ end
279
+
280
+ def claim_pidfile
281
+ File.open(@pidfile, File::WRONLY | File::CREAT | File::EXCL, 0o644) do |file|
282
+ file.write("#{Process.pid}\n")
283
+ end
284
+ end
285
+
286
+ # @raise [Kino::Error] when the pidfile's owner is still alive, or the
287
+ # file does not look like a pidfile at all
288
+ def refuse_unless_stale
289
+ content = begin
290
+ File.read(@pidfile)
291
+ rescue Errno::ENOENT
292
+ return # already gone; nothing to refuse
293
+ end
294
+ pid = Integer(content.strip, exception: false)
295
+ unless pid&.positive?
296
+ raise Error, "refusing to overwrite #{@pidfile}: does not hold a pid"
297
+ end
298
+ raise Error, "already running (pid #{pid}, per #{@pidfile})" if process_alive?(pid)
299
+ end
300
+
301
+ def process_alive?(pid)
302
+ Process.kill(0, pid)
303
+ true
304
+ rescue Errno::ESRCH
305
+ false
306
+ rescue Errno::EPERM
307
+ true # exists, just not ours to signal
308
+ end
309
+
310
+ # Delete only a pidfile that is still ours: by shutdown time the path
311
+ # may belong to a replacement instance, or an operator may have
312
+ # repointed it at something that is not a pidfile at all.
313
+ def remove_pidfile
314
+ File.unlink(@pidfile) if File.read(@pidfile) == "#{Process.pid}\n"
315
+ rescue Errno::ENOENT
316
+ nil
317
+ end
318
+
244
319
  def join_workers(deadline)
245
320
  if @supervisor
246
321
  @supervisor.shutdown([deadline - monotonic_now, 0].max)
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.3"
5
+ VERSION = "0.2.1"
6
6
  end
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.1.3
4
+ version: 0.2.1
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-07-04 00:00:00.000000000 Z
11
+ date: 2026-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger