kino 0.3.0-aarch64-linux → 0.4.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: 18bb12fa02179c4c2e164cf83e605ac893094650fcaee119132b48b2eea0ae2e
4
- data.tar.gz: 56c6652d6671f2872e0399f82a789e6b4cc5b0ea018f5503bb91ed575f187985
3
+ metadata.gz: 145273ae455b0822a24794c25d449376b3ba5122f3f275c00b4a959fdafd5183
4
+ data.tar.gz: c20ae1c2f111a4f6329ea25a4d9c9a0579dfa1b77765ff11e35572f8e32925ca
5
5
  SHA512:
6
- metadata.gz: 1c3f92ba0bd339f2415ae94024f4afc5666a356c69b7721bb36deeccc492521d7f75a5ef064f994306a771924a42ccb00ff6e3ce138754b797ef5e2263144622
7
- data.tar.gz: ae5f7535f0fc9ef4b9398a1fdd57723aae445d49ef7705fb1cab907d6b8ab01bb25338394509467e7cc755b37105b306ab08f89f755d4496e186e63f32cd6ace
6
+ metadata.gz: ed2ef3b3d79817b67da62e8271f0a420d4ee0c8976780d97e6e91ba15cddff2e1d34e830d41a8c4c8c63d65fecbeab32bec8d8568777b399c684cdfce00624f0
7
+ data.tar.gz: 6d695d16998c2bdb9fe44ac709a311990c53c88ae980f136757db926ba370e5a8e379fdd098f675beae4fb126a0ce7d663b36b0a602d6933b1c2b6d3f6cdc02b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
1
+ ## [0.4.0] - 2026-08-22
2
+
3
+ - Rack handler: `rails server -u kino` and `rackup -s kino` boot Kino
4
+ through `Rackup::Handler::Kino`, reading the same config file as the
5
+ `kino` CLI with the host's flags on top; `rackup -s kino --help` lists
6
+ the `-O` options (Workers, Threads, Mode, Config).
7
+ - The config file is also looked up at `config/kino.rb` (the Rails
8
+ layout) when there is no `kino.rb`, by the CLI and the handler alike.
9
+ - `Kino::Server#run` serves an already built server the way
10
+ `Kino::Server.run` does (banner, signal traps, block until shutdown),
11
+ and syncs stdout there so the banner is never held back by block
12
+ buffering under a pipe, whichever entry point booted the server.
13
+ - `workers` now defaults to `Kino.available_parallelism`, the CPUs the
14
+ process may actually use: the affinity mask and, in a container, the
15
+ cgroup CPU quota (a pod limited to 2 CPUs on a 64-core node gets 2
16
+ workers, not 64). `Etc.nprocessors` only ever saw the mask.
17
+ - `bind "unix:///path/to.sock"` listens on a unix domain socket, the
18
+ usual shape behind nginx: a stale socket file is reclaimed, a live one
19
+ is refused, and the file is removed on shutdown. `port` is unused on
20
+ it and TLS is rejected (terminate TLS at the proxy). Requests arriving
21
+ over the socket report `REMOTE_ADDR` 127.0.0.1.
22
+ - `Kino::Server#url`, `#control_url`, and `#unix?` report where a started
23
+ server and its control plane listen.
24
+ - The access log is two records per request: an arrival line queued
25
+ before the app runs (a hang shows as an arrow with no answer) and a
26
+ status-tinted completion line with a timing breakdown of `ruby`,
27
+ `kino`, and `wait`, the `ruby` part carrying the GC pause and objects
28
+ allocated where one request at a time can own the VM's counters
29
+ (`:threaded`, or `:ractor` with `workers 1`). Local timestamps with
30
+ their UTC offset; a blank line between requests. The former one-line
31
+ format is gone.
32
+ - A failed request is reported as `500 GET /path · Class: message (site)`
33
+ followed by its backtrace relative to the working directory, the app's
34
+ own frames first, the rest folded into `… N more`.
35
+ - Every line Kino logs about itself (draining, a crash and its respawn,
36
+ hook failures, quarantine, the USR1 stats line, `rack.errors`) reads
37
+ `kino[<pid>] <source>: message`, the source naming the worker that
38
+ spoke (`worker-3`, `worker-3/thread-2`) or `main`; worker ractors and
39
+ threads now carry those names. The label is dim, yellow, or red by
40
+ level on color terminals. `Kino::Log.info`, `.warn`, and `.error` are
41
+ public, for hooks.
42
+ - The startup banner lists the Ruby build with its JIT and parser flags,
43
+ the environment, the topology, the pid, and the control-plane address
44
+ when one is bound.
45
+
1
46
  ## [0.3.0] - 2026-08-13
2
47
 
3
48
  - Queue-time histogram: `/metrics` exposes `kino_request_queue_seconds`, a
data/README.md CHANGED
@@ -196,6 +196,12 @@ bundle exec kino # picks up config.ru + kino.rb, serves on :9292
196
196
  (After a standalone `gem install`, the `kino` command works without
197
197
  `bundle exec`.)
198
198
 
199
+ Prefer your framework's own command? Kino ships a Rack handler, so
200
+ `rails server -u kino` and `rackup -s kino` boot it too. They read the
201
+ same `kino.rb` (or `config/kino.rb`), the host's `-p`/`-b` flags win over
202
+ the file, and `rackup -s kino -O Workers=4 -O Mode=threaded` reaches the
203
+ rest (`rackup -s kino --help` lists them).
204
+
199
205
  No Rust compiler needed: released versions ship precompiled native gems
200
206
  for Linux (x86_64/aarch64, glibc and musl) and macOS (arm64). On other
201
207
  platforms the gem compiles at install time; that needs a Rust toolchain,
@@ -218,9 +224,9 @@ Or embedded, with everything spelled out:
218
224
 
219
225
  ```ruby
220
226
  server = Kino::Server.new(app,
221
- bind: "127.0.0.1",
227
+ bind: "127.0.0.1", # or "unix:///run/kino.sock" behind a proxy
222
228
  port: 9292, # 0 = ephemeral; read back via server.port
223
- workers: Etc.nprocessors, # ractors (parallelism)
229
+ workers: Kino.available_parallelism, # ractors (parallelism); the default
224
230
  threads: 1, # per worker; ractor default 1, threaded default 3
225
231
  mode: :auto, # :auto | :ractor | :threaded
226
232
  queue_depth: 1024, # bounded queue; overflow → 503
@@ -255,8 +261,10 @@ server.shutdown # graceful: drain → deadline → abort straggler
255
261
 
256
262
  ## Config file and CLI
257
263
 
258
- Settings can live in a Puma-style Ruby DSL file. Precedence: explicit
259
- kwargs and CLI flags > config file > defaults.
264
+ Settings can live in a Puma-style Ruby DSL file: `kino.rb` in the
265
+ working directory, or `config/kino.rb` (the Rails layout), is picked up
266
+ automatically; `-C PATH` names any other. Precedence: explicit kwargs
267
+ and CLI flags > config file > defaults.
260
268
 
261
269
  ```ruby
262
270
  # kino.rb
@@ -374,11 +382,11 @@ server.stats
374
382
  # plus lane_depths: [...] when lane dispatch is on
375
383
  ```
376
384
 
377
- From the outside, `kill -USR1 <pid>` prints the same snapshot as one line
385
+ From the outside, `kill -USR1 <pid>` logs the same snapshot as one line
378
386
  (pair it with `pidfile` to find the pid):
379
387
 
380
388
  ```
381
- Kino stats: mode=:ractor lanes=false workers=8 threads=1 batch=1 respawns=0 queued=0 in_flight=2 served=1041 rejected=0 timeouts=0
389
+ kino[4213] main: stats mode=:ractor lanes=false workers=8 threads=1 batch=1 respawns=0 queued=0 in_flight=2 served=1041 rejected=0 timeouts=0
382
390
  ```
383
391
 
384
392
  For pull-based monitoring, `control_bind "127.0.0.1:9293"` (or a
@@ -422,16 +430,28 @@ box). There are two native pieces. Both write through a lock-free
422
430
  channel to a Rust flusher thread, so request threads never take a log
423
431
  mutex and never make a write syscall:
424
432
 
425
- - **Access log** (`log_requests true`): one line per request to stdout,
426
- including the 503s that never reach your app. Recommended in
427
- development; cheap enough for production. On color terminals the
428
- lines are tinted by status class: 2xx green, 3xx yellow, 4xx maroon,
429
- 5xx bright red:
433
+ - **Access log** (`log_requests true`): two records per request to
434
+ stdout, including the 503s that never reach your app. The arrival line
435
+ is queued before the app runs, so a request that hangs shows as an
436
+ arrow with no answer; the completion line carries the status, the
437
+ total, and a timing breakdown: `ruby` is the time the request spent in
438
+ Ruby (with the GC pause and the objects allocated during it), `kino`
439
+ the server's own overhead, `wait` the queue time before a worker took
440
+ it. Recommended in development; cheap enough for production. On color
441
+ terminals the completion line is tinted by status class: 2xx green,
442
+ 3xx yellow, 4xx maroon, 5xx bright red:
430
443
 
431
444
  ```
432
- 127.0.0.1 [Tue, 10 Jun 2026 13:39:56 GMT] "GET / HTTP/1.1" 200 0.1ms
445
+ 2026-08-22 14:03:11 +0300 GET /users?q=1 from 127.0.0.1
446
+ 2026-08-22 14:03:11 +0300 ← 200 GET /users?q=1 12.4ms (ruby 9.1ms [gc 0.8ms; 1.5k obj]; kino 3.2ms; wait 0.1ms)
433
447
  ```
434
448
 
449
+ The GC and allocation figures come from the VM's process-wide
450
+ counters, so they appear only where one request at a time can own
451
+ them: in `:threaded` mode, or in `:ractor` mode with `workers 1`.
452
+ Parallel ractors would bill each other's work, so there the breakdown
453
+ is `(ruby; kino; wait)` alone.
454
+
435
455
  - **`Kino::Logger`**: a `::Logger` over the same async sink, for your
436
456
  app's own logging (`Kino::Logger.new("log/production.log")`, or no
437
457
  argument for stdout). The raw IO-like device is `Kino::Logger::Device`,
@@ -473,6 +493,27 @@ lines/s), the sink drops lines instead of blocking request threads.
473
493
  These trade-offs are measured in
474
494
  [doc/benchmarks.md](doc/benchmarks.md#logging-costs).
475
495
 
496
+ **Server lines.** Everything Kino says about itself (draining, a crash
497
+ and its respawn, a hook that raised, quarantine, the stats line,
498
+ `rack.errors`) reads `kino[<pid>] <source>: message`, the source being
499
+ the worker that spoke, `worker-3` (or `worker-3/thread-2` in a
500
+ multi-threaded ractor), or `main`. On color terminals the label is dim
501
+ for notes, yellow for warnings, red for errors; the message stays plain.
502
+ A failed request gets a report instead of a bare backtrace: the request
503
+ line, the error, and where it raised in your code, then the trace with
504
+ your frames first, relative to the working directory, and the rest
505
+ folded:
506
+
507
+ ```
508
+ kino[4213] worker-2: 500 GET /boom · RuntimeError: kaboom (app.rb:12:in 'explode')
509
+ app.rb:12:in 'explode'
510
+ /usr/lib/ruby/gems/4.0.0/gems/rack-3.2.7/lib/rack/builder.rb:...
511
+ … 38 more
512
+ ```
513
+
514
+ Hooks can log through the same channel with `Kino::Log.info`, `.warn`,
515
+ and `.error`; it is safe inside worker ractors.
516
+
476
517
  ## Timer waits
477
518
 
478
519
  `Kino.sleep(seconds)` is a high-resolution sleep on the OS clock with
@@ -491,8 +532,9 @@ optional in Rack 3.
491
532
 
492
533
  ## Rails
493
534
 
494
- Rails (edge) runs on Kino today in `:threaded` mode; see
495
- `examples/rails-hello`. Ractor-mode Rails is blocked upstream. The exact
535
+ Rails (edge) runs on Kino today in `:threaded` mode (`rails server -u
536
+ kino`, or the `kino` CLI); see `examples/rails-hello`. Ractor-mode Rails
537
+ is blocked upstream. The exact
496
538
  blockers, the `Ruby::Box` findings, and what would unlock it are written
497
539
  up in [doc/rails-on-ractors.md](doc/rails-on-ractors.md). The example
498
540
  ships a probe script that re-tests against whatever Rails you bundle.
data/exe/kino CHANGED
@@ -9,10 +9,6 @@
9
9
  # process-global bits an executable owns.
10
10
 
11
11
  Warning[:experimental] = false
12
- # Startup output must land immediately even when stdout is a pipe or file
13
- # (process supervisors, `kino > server.log`); block buffering would hold
14
- # the banner back until exit.
15
- $stdout.sync = true
16
12
 
17
13
  # Running from a git checkout (no installed gem, no bundler context):
18
14
  # prefer the checkout's own lib so `require "kino"` resolves.
data/lib/kino/cli.rb CHANGED
@@ -100,14 +100,14 @@ module Kino
100
100
  end.join
101
101
  end
102
102
 
103
- # One-line stats dump (the SIGUSR1 handler's output). Excludes
103
+ # One-line stats dump (what the SIGUSR1 handler logs). Excludes
104
104
  # worker_status: it's an array with one entry per execution slot, and
105
105
  # printing it inline would break the one-line contract (see /stats for
106
106
  # per-worker detail).
107
107
  # @param stats [Hash{Symbol => Object}] see {Kino::Server#stats}
108
108
  # @return [String]
109
109
  def stats_line(stats)
110
- dim("Kino stats: #{stats.except(:worker_status).map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}")
110
+ "stats #{stats.except(:worker_status).map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}"
111
111
  end
112
112
 
113
113
  # The two banner halves around Server#start: credits before, the ready
@@ -119,15 +119,27 @@ module Kino
119
119
  puts dim("\nKino #{VERSION} presents:")
120
120
  end
121
121
 
122
+ # The ready block: what this process is (Ruby build with its JIT and
123
+ # parser flags, environment, topology, pid) and where it listens.
122
124
  # @param server [Kino::Server] a started server
123
125
  # @return [void]
124
126
  def action!(server)
125
- puts dim("- mode: #{server.mode}")
126
- puts dim("- listening: http#{"s" if server.tls?}://#{server.bind}:#{server.port}")
127
+ stats = server.stats
128
+ puts dim("- ruby: #{RUBY_DESCRIPTION}")
129
+ puts dim("- env: #{ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"}")
130
+ puts dim("- mode: #{server.mode}, #{count(stats[:workers], "worker")} × #{count(stats[:threads], "thread")}")
131
+ puts dim("- pid: #{Process.pid}")
132
+ puts dim("- listening: #{server.url}")
133
+ puts dim("- control: #{server.control_url}") if server.control_url
127
134
  puts dim("- Ctrl-C to drain and stop")
128
135
  puts "\n#{bold("Action!")}\n\n"
129
136
  end
130
137
 
138
+ # "1 worker", "8 workers".
139
+ def count(number, noun)
140
+ "#{number} #{noun}#{"s" unless number == 1}"
141
+ end
142
+
131
143
  # Roll credits when the process ends: normal exit or crash (at_exit
132
144
  # also runs after an uncaught exception; only a force-exit skips it).
133
145
  # @return [void]
@@ -200,7 +212,7 @@ module Kino
200
212
  def option_parser(options)
201
213
  OptionParser.new do |opts|
202
214
  opts.banner = "Usage: kino [options] [rackup file (default: config.ru)]"
203
- opts.on("-C", "--config FILE", "Config file (default: kino.rb if present)") { |v| options[:config_file] = v }
215
+ opts.on("-C", "--config FILE", "Config file (default: kino.rb, then config/kino.rb)") { |v| options[:config_file] = v }
204
216
  opts.on("--init [PATH]", "Write a commented sample config (default: kino.rb) and exit") do |v|
205
217
  options[:init_path] = v || "kino.rb"
206
218
  end
@@ -236,14 +248,13 @@ module Kino
236
248
  require "kino"
237
249
  require "rack"
238
250
 
239
- config_file = options[:config_file]
240
- config_file ||= ("kino.rb" if File.exist?("kino.rb"))
251
+ config_file = options[:config_file] || Configuration.default_path
241
252
 
242
253
  config = Configuration.new
243
254
  config.load_file(config_file) if config_file
244
255
  config.merge!(options[:overrides])
245
256
  # Default port 9292 when neither the file nor a flag chose one.
246
- config.set(:port, 9292) unless config.set?(:port)
257
+ config.set(:port, Configuration::DEFAULT_SERVING_PORT) unless config.set?(:port)
247
258
  config
248
259
  end
249
260
 
@@ -252,6 +263,6 @@ module Kino
252
263
  end
253
264
 
254
265
  private_class_method :print_help, :option_parser, :write_sample,
255
- :resolve_config, :serve
266
+ :resolve_config, :serve, :count
256
267
  end
257
268
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "etc"
4
-
5
3
  module Kino
6
4
  # Server settings with Puma-style precedence:
7
5
  # explicit Server.new kwargs > config file DSL > defaults.
@@ -11,7 +9,7 @@ module Kino
11
9
  DEFAULTS = {
12
10
  bind: "127.0.0.1",
13
11
  port: 0,
14
- workers: nil, # resolved to Etc.nprocessors in #to_h
12
+ workers: nil, # resolved to Kino.available_parallelism in #to_h
15
13
  threads: nil, # resolved per mode in Server: 1 in :ractor, 3 in :threaded
16
14
  mode: :auto,
17
15
  queue_depth: 1024,
@@ -45,6 +43,21 @@ module Kino
45
43
  # Source template for {.sample}.
46
44
  SAMPLE_TEMPLATE = File.expand_path("templates/kino.rb.tt", __dir__)
47
45
 
46
+ # Where the `kino` CLI and the Rack handler look for a config file when
47
+ # none is named: the project root first, then the Rails-style config/.
48
+ DEFAULT_PATHS = %w[kino.rb config/kino.rb].freeze
49
+
50
+ # The port the CLI and the Rack handler serve on when neither a flag
51
+ # nor the file chose one (Server.new itself defaults to an ephemeral
52
+ # port, for embedding).
53
+ DEFAULT_SERVING_PORT = 9292
54
+
55
+ # The first of {DEFAULT_PATHS} that exists in the working directory.
56
+ # @return [String, nil]
57
+ def self.default_path
58
+ DEFAULT_PATHS.find { |path| File.exist?(path) }
59
+ end
60
+
48
61
  # The fully-commented sample config (see `kino --init`).
49
62
  # @return [String]
50
63
  def self.sample
@@ -113,7 +126,7 @@ module Kino
113
126
  # @return [Hash{Symbol => Object}] every setting, defaults filled in
114
127
  def to_h
115
128
  SETTINGS.to_h { |key| [key, self[key]] }.tap do |h|
116
- h[:workers] ||= Etc.nprocessors
129
+ h[:workers] ||= Kino.available_parallelism
117
130
  end
118
131
  end
119
132
 
@@ -146,7 +159,9 @@ module Kino
146
159
  @config = config
147
160
  end
148
161
 
149
- # Address to listen on ("0.0.0.0" accepts non-local connections).
162
+ # Address to listen on: a host ("0.0.0.0" accepts non-local
163
+ # connections), or "unix:///path/to.sock" for a unix domain socket
164
+ # (then `port` is unused).
150
165
  def bind(host) = @config.set(:bind, host)
151
166
 
152
167
  # Port to listen on; 0 picks an ephemeral port.
@@ -2,17 +2,18 @@
2
2
 
3
3
  module Kino
4
4
  # @private
5
- # rack.errors: stateless writer into the native logger. Frozen singleton,
5
+ # rack.errors: stateless writer into the server log (one line per
6
+ # call, labelled like every other line Kino writes). Frozen singleton,
6
7
  # which also makes it Ractor-shareable; one instance serves all workers.
7
8
  class ErrorsStream
8
9
  def puts(message)
9
- Native.log_error(message.to_s)
10
+ Log.error(message.to_s.chomp)
10
11
  nil
11
12
  end
12
13
 
13
14
  def write(message)
14
15
  message = message.to_s
15
- Native.log_error(message)
16
+ Log.error(message.chomp)
16
17
  message.bytesize
17
18
  end
18
19
 
@@ -4,9 +4,8 @@ module Kino
4
4
  # @private
5
5
  # Fires a lifecycle hook and turns a raise into a logged line instead of
6
6
  # letting it escape. Stateless and touches only its arguments plus
7
- # Native.log_error (already called from inside worker ractors today), so
8
- # it is safe to call from worker context: no main-ractor state is
9
- # captured.
7
+ # Kino::Log (safe inside worker ractors), so it is safe to call from
8
+ # worker context: no main-ractor state is captured.
10
9
  module HookFire
11
10
  module_function
12
11
 
@@ -16,7 +15,7 @@ module Kino
16
15
  begin
17
16
  hook.call(*args)
18
17
  rescue => e
19
- Native.log_error("#{name} hook raised #{e.class}: #{e.message}")
18
+ Log.error("#{name} hook raised #{e.class}: #{e.message}")
20
19
  end
21
20
  end
22
21
  end
data/lib/kino/kino.so CHANGED
Binary file
data/lib/kino/log.rb ADDED
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kino
4
+ # Server log lines: the lifecycle notices, crash and respawn reports,
5
+ # hook failures, the failed-request report, and whatever apps write to
6
+ # `rack.errors`, all in one shape:
7
+ #
8
+ # kino[4213] worker-3: after_worker_boot hook raised RuntimeError: boom
9
+ #
10
+ # The label is syslog's `ident[pid]` tag plus the source that spoke: the
11
+ # worker ractor and/or thread by name, `main` for neither. On color
12
+ # terminals the label is dim, yellow, or red by level; the message stays
13
+ # plain. Notes go to stdout, warnings and errors to stderr.
14
+ #
15
+ # Hooks may log through here too (`Kino::Log.info "cache warm"`). Every
16
+ # method is safe inside a worker ractor: the line is handed to the
17
+ # native layer, which owns the streams, so no ractor touches `$stdout`
18
+ # or `$stderr` itself.
19
+ module Log
20
+ # Frames shown in a failed-request report before the rest are folded.
21
+ FRAMES = 12
22
+
23
+ # The working directory at boot, stripped from backtrace frames so the
24
+ # app's own code reads `app/controllers/x.rb:9` rather than an
25
+ # absolute path (frozen: worker ractors read it).
26
+ WORKING_DIR = File.join(Dir.pwd, "").freeze
27
+
28
+ module_function
29
+
30
+ # @param message [#to_s]
31
+ # @return [void]
32
+ def info(message)
33
+ Native.log_line("info", source, message.to_s)
34
+ end
35
+
36
+ # @param message [#to_s]
37
+ # @return [void]
38
+ def warn(message)
39
+ Native.log_line("warn", source, message.to_s)
40
+ end
41
+
42
+ # @param message [#to_s]
43
+ # @return [void]
44
+ def error(message)
45
+ Native.log_line("error", source, message.to_s)
46
+ end
47
+
48
+ # The failed-request report: the request line, the error, and where it
49
+ # raised in the app, then the backtrace with the app's own frames
50
+ # first (relative to the working directory) and the rest folded.
51
+ #
52
+ # 500 GET /boom · RuntimeError: kaboom (app.rb:12:in 'explode')
53
+ # app.rb:12:in 'explode'
54
+ # /gems/rack-3.2.7/lib/rack/builder.rb:...
55
+ # … 38 more
56
+ #
57
+ # @param error [Exception]
58
+ # @param env [Hash] the Rack env of the failed request
59
+ # @param status [Integer] the status the client got
60
+ # @return [void]
61
+ def exception(error, env, status: 500)
62
+ frames, depth = trace(error)
63
+ site = frames.first ? " (#{frames.first})" : ""
64
+ lines = ["#{status} #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]} · #{error.class}: #{error.message}#{site}"]
65
+ frames.each { |frame| lines << " #{frame}" }
66
+ lines << " … #{depth - frames.size} more" if depth > frames.size
67
+ error(lines.join("\n"))
68
+ end
69
+
70
+ # The `kino[<pid>] <source>:` tag a line from here carries.
71
+ # @return [String]
72
+ def label
73
+ "kino[#{Process.pid}] #{source}:"
74
+ end
75
+
76
+ # Who is speaking: the ractor's name, the thread's name, both joined
77
+ # with a slash, or `main` when neither is named. Kino names its
78
+ # worker ractors and threads `worker-N`.
79
+ # @return [String]
80
+ def source
81
+ parts = [Ractor.current.name, Thread.current.name].compact
82
+ parts.empty? ? "main" : parts.join("/")
83
+ end
84
+
85
+ # The backtrace as [frames, depth]: each frame relativized to the
86
+ # working directory, the app's own frames floated to the front (the
87
+ # raise site in your code reads first; gem and stdlib frames keep
88
+ # their order below), capped at FRAMES; depth is the real length.
89
+ def trace(error)
90
+ raw = error.backtrace || []
91
+ app, rest = raw.map { |frame| frame.delete_prefix(WORKING_DIR) }.partition { |frame| app_frame?(frame) }
92
+ [(app + rest).first(FRAMES), raw.size]
93
+ end
94
+
95
+ # A project-relative path (the working-directory prefix came off, so
96
+ # it does not start with `/`) that is not a synthetic frame (`(eval)`,
97
+ # `<internal:...>`). Gem and stdlib frames stay absolute.
98
+ def app_frame?(frame)
99
+ !frame.start_with?("/", "<", "(")
100
+ end
101
+
102
+ private_class_method :trace, :app_frame?
103
+ end
104
+ end
@@ -21,7 +21,10 @@ module Kino
21
21
 
22
22
  def start
23
23
  @running = true
24
- @thread = Thread.new { run }
24
+ @thread = Thread.new do
25
+ Thread.current.name = "quarantine"
26
+ run
27
+ end
25
28
  self
26
29
  end
27
30
 
@@ -35,14 +38,14 @@ module Kino
35
38
  def run
36
39
  tick while @running
37
40
  rescue => e
38
- Native.log_error("quarantine monitor crashed: #{e.class}: #{e.message}")
41
+ Log.error("quarantine monitor crashed: #{e.class}: #{e.message}")
39
42
  end
40
43
 
41
44
  def tick
42
45
  scan_slots
43
46
  rescue => e
44
47
  # A bad tick must never kill the monitor.
45
- Native.log_error("quarantine tick error: #{e.class}: #{e.message}")
48
+ Log.error("quarantine tick error: #{e.class}: #{e.message}")
46
49
  ensure
47
50
  sleep @tick
48
51
  end
@@ -53,7 +56,7 @@ module Kino
53
56
 
54
57
  if @outstanding >= @max
55
58
  unless @at_cap_logged
56
- Native.log_error("quarantine at cap (#{@max}); serving at reduced capacity")
59
+ Log.warn("quarantine at cap (#{@max}); serving at reduced capacity")
57
60
  @at_cap_logged = true
58
61
  end
59
62
  next
@@ -91,6 +91,7 @@ module Kino
91
91
 
92
92
  def supervise(index)
93
93
  Thread.new do
94
+ Thread.current.name = "supervisor-#{index}"
94
95
  crashes = 0
95
96
  loop do
96
97
  ractor, worker_ids = spawn_worker(index)
@@ -109,7 +110,7 @@ module Kino
109
110
 
110
111
  crashes += 1
111
112
  Native.record_respawn(@server_id)
112
- Native.log_error("worker ractor #{index} crashed (#{cause.class}: #{cause.message}); respawning")
113
+ Log.error("worker-#{index} crashed (#{cause.class}: #{cause.message}); respawning")
113
114
  # Policy (crash recovery): unlimited respawn
114
115
  # keeps the server up under rare crashes but turns a
115
116
  # crash-on-every-request bug into a busy loop. A circuit breaker
@@ -129,12 +130,16 @@ module Kino
129
130
  @worker_slots[worker_index] = worker_ids
130
131
  worker_ids.each { |id| @slot_to_worker[id] = worker_index }
131
132
  end
132
- ractor = Ractor.new(@server_id, worker_ids, @app, @batch, @hooks) do |server_id, ids, app, batch, hooks|
133
- ids.map do |id|
133
+ # Named so log lines from inside say which worker spoke: the ractor
134
+ # alone for a single thread, `worker-N/thread-M` for more.
135
+ ractor = Ractor.new(@server_id, worker_ids, @app, @batch, @hooks,
136
+ name: "worker-#{worker_index}") do |server_id, ids, app, batch, hooks|
137
+ ids.each_with_index.map do |id, position|
134
138
  Thread.new do
135
139
  # Crashes surface via Ractor#value in the supervisor; don't also
136
140
  # spray the backtrace to stderr from inside the dying ractor.
137
141
  Thread.current.report_on_exception = false
142
+ Thread.current.name = "thread-#{position + 1}" if ids.size > 1
138
143
  Kino::Worker.run(server_id, id, app, batch, hooks)
139
144
  end
140
145
  end.each(&:join)
data/lib/kino/server.rb CHANGED
@@ -27,6 +27,29 @@ module Kino
27
27
  !@tls.nil?
28
28
  end
29
29
 
30
+ # @return [Boolean] whether the bind is a unix domain socket
31
+ # ("unix:///path/to.sock")
32
+ def unix?
33
+ @bind.start_with?("unix://")
34
+ end
35
+
36
+ # Where the server listens, once started: `http://host:port`
37
+ # (`https` under TLS), or the `unix://` socket path.
38
+ # @return [String]
39
+ def url
40
+ unix? ? @bind : "http#{"s" if tls?}://#{@bind}:#{@port}"
41
+ end
42
+
43
+ # Where the control plane listens, once started, or nil when it is
44
+ # off: `http://host:port`, or its `unix://` socket path.
45
+ # @return [String, nil]
46
+ def control_url
47
+ return nil unless @control_bind
48
+ return @control_bind if @control_bind.start_with?("unix://")
49
+
50
+ "http://#{@control_bind.rpartition(":").first}:#{@control_port}"
51
+ end
52
+
30
53
  # Settings precedence: explicit kwargs > config_file DSL > defaults.
31
54
  #
32
55
  # @param app [#call] a Rack 3 application
@@ -54,7 +77,12 @@ module Kino
54
77
  @worker_hooks = WorkerHooks.new(
55
78
  on_error: @on_error,
56
79
  after_worker_boot: @after_worker_boot,
57
- after_request_complete: @after_request_complete
80
+ after_request_complete: @after_request_complete,
81
+ # The access log's GC and allocation figures come from the VM's
82
+ # process-wide counters, so they are measured only where one
83
+ # request at a time can own them: the GVL serializes :threaded
84
+ # mode, and a single ractor has nothing to race.
85
+ access_timing: !!settings[:log_requests] && (@mode == :threaded || @workers == 1)
58
86
  )
59
87
  # Default threads per mode: 1 in :ractor (threads inside a ractor
60
88
  # share its lock; a measured +17% on fast handlers; raise `workers`
@@ -72,6 +100,9 @@ module Kino
72
100
  @shutdown_timeout = settings[:shutdown_timeout]
73
101
  @tokio_threads = settings[:tokio_threads]
74
102
  @tls = validate_tls(settings[:tls])
103
+ if @tls && unix?
104
+ raise ArgumentError, "TLS is not supported on a unix socket bind; terminate TLS at the proxy in front"
105
+ end
75
106
  @pidfile = settings[:pidfile]
76
107
  @control_bind = settings[:control_bind]&.to_s
77
108
  @control_token = settings[:control_token]&.to_s
@@ -195,22 +226,34 @@ module Kino
195
226
  @supervisor ? @supervisor.join : @worker_threads.each(&:join)
196
227
  end
197
228
 
198
- # Production entry point: start, print the banner, trap INT/TERM for
199
- # graceful shutdown (second signal force-exits), block until done.
200
- # The `kino` CLI funnels into this too (CLI#serve).
229
+ # Production entry point: build the server and {#run} it. The `kino`
230
+ # CLI funnels into this too (CLI#serve).
201
231
  #
202
232
  # @param app [#call] a Rack 3 application
203
233
  # @param opts [Hash] see #initialize
204
234
  # @return [Kino::Server] the (stopped) server, after shutdown
205
235
  def self.run(app, **opts)
206
- server = new(app, **opts)
236
+ new(app, **opts).run
237
+ end
238
+
239
+ # Serve until shut down: start, print the banner, trap INT/TERM for
240
+ # graceful shutdown (second signal force-exits), block until done.
241
+ # The Rack handler calls this on a server it built itself.
242
+ #
243
+ # @return [self] after shutdown
244
+ def run
245
+ # Startup output must land immediately even when stdout is a pipe or
246
+ # file (process supervisors, `kino > server.log`, `rails server`
247
+ # under Docker); block buffering would hold the banner back until
248
+ # exit.
249
+ $stdout.sync = true
207
250
  CLI.opening_credits
208
- server.start
209
- CLI.action!(server)
251
+ start
252
+ CLI.action!(self)
210
253
  CLI.fin_at_exit
211
- trap_signals(server)
212
- server.wait
213
- server
254
+ self.class.trap_signals(self)
255
+ wait
256
+ self
214
257
  end
215
258
 
216
259
  # Signal handling shared by Server.run and the kino CLI: INT/TERM drain
@@ -222,14 +265,14 @@ module Kino
222
265
  # kill -USR1 <pid> prints a one-line stats snapshot (find the pid in
223
266
  # the pidfile when configured).
224
267
  trap("USR1") do
225
- Thread.new { $stdout.puts Kino::CLI.stats_line(server.stats) }
268
+ Thread.new { Log.info(CLI.stats_line(server.stats)) }
226
269
  end
227
270
  signaled = false
228
271
  %w[INT TERM].each do |signal|
229
272
  trap(signal) do
230
273
  Process.exit!(1) if signaled
231
274
  signaled = true
232
- $stderr.write("Kino: draining (signal again to force exit)\n")
275
+ Log.warn("draining (signal again to force exit)")
233
276
  # Trap context forbids mutexes; do the real work on a thread.
234
277
  Thread.new { server.shutdown }
235
278
  end
@@ -270,6 +313,8 @@ module Kino
270
313
  def spawn_worker_thread
271
314
  worker_id = Native.register_worker(@id)
272
315
  Thread.new do
316
+ # Named so log lines from inside say which worker spoke.
317
+ Thread.current.name = "worker-#{worker_id}"
273
318
  error = nil
274
319
  begin
275
320
  Worker.run(@id, worker_id, @app, @batch, @worker_hooks)
@@ -442,7 +487,7 @@ module Kino
442
487
  if @supervisor
443
488
  # Ractors cannot be force-killed; their clients were already freed
444
489
  # by abort_all_inflight. The stuck ractor leaks until process exit.
445
- Native.log_error("shutdown deadline passed with stuck ractor workers") unless @supervisor.done?
490
+ Log.error("shutdown deadline passed with stuck ractor workers") unless @supervisor.done?
446
491
  else
447
492
  threads = @worker_threads_lock.synchronize { @worker_threads.dup }
448
493
  threads.each { |thread| thread.kill if thread.alive? }
@@ -474,10 +519,10 @@ module Kino
474
519
  :ractor
475
520
  when :auto
476
521
  if !Ractor.shareable?(@app)
477
- warn "Kino: app is not Ractor-shareable; falling back to mode: :threaded"
522
+ Log.warn("app is not Ractor-shareable; falling back to mode: :threaded")
478
523
  :threaded
479
524
  elsif (name = unshareable_worker_hook_name)
480
- warn "Kino: #{name} hook is not Ractor-shareable; falling back to mode: :threaded"
525
+ Log.warn("#{name} hook is not Ractor-shareable; falling back to mode: :threaded")
481
526
  :threaded
482
527
  else
483
528
  :ractor
@@ -8,7 +8,9 @@
8
8
  ## Network
9
9
 
10
10
  # Address to listen on. Use "0.0.0.0" to accept connections from other
11
- # machines.
11
+ # machines, or "unix:///run/kino.sock" to listen on a unix domain socket
12
+ # behind a proxy such as nginx (the port below is then unused; a stale
13
+ # socket file is reclaimed, a live one refused).
12
14
  # bind "127.0.0.1"
13
15
 
14
16
  # Port to listen on.
@@ -22,7 +24,8 @@
22
24
 
23
25
  # How many workers to run. Each worker handles requests independently;
24
26
  # in :ractor mode every worker runs Ruby in parallel on its own core.
25
- # Default: one per CPU core.
27
+ # Default: the CPUs this process may use (Kino.available_parallelism:
28
+ # the affinity mask and, in a container, the cgroup CPU quota).
26
29
  # workers 8
27
30
 
28
31
  # Threads inside each worker. More threads help when your app spends
@@ -74,9 +77,12 @@
74
77
  # for quick handlers; behavior under heavy overload differs slightly.
75
78
  # lanes false
76
79
 
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.
80
+ # Log every request to stdout: an arrival line before the app runs and a
81
+ # completion line after it, colored by status on a terminal, with a
82
+ # timing breakdown (time in Ruby with its GC pause and allocations, the
83
+ # server's own overhead, and queue wait). This is the server's view: it
84
+ # includes requests your app never saw, such as 503s. Recommended in
85
+ # development; cheap enough for production.
80
86
  # log_requests false
81
87
 
82
88
  # Called when a worker catches an app or delivery error, after the client
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.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
data/lib/kino/worker.rb CHANGED
@@ -70,7 +70,16 @@ module Kino
70
70
  def serve(env, app, hooks)
71
71
  request = env[KINO_REQUEST]
72
72
  env[RACK_INPUT] ||= Input.new(request)
73
- status, headers, body = app.call(env)
73
+ if hooks&.access_timing
74
+ # The access log's breakdown: the VM's cumulative GC time and
75
+ # allocation count, differenced around the app call.
76
+ gc_before = GC.total_time
77
+ allocated_before = GC.stat(:total_allocated_objects)
78
+ status, headers, body = app.call(env)
79
+ request.timing(GC.total_time - gc_before, GC.stat(:total_allocated_objects) - allocated_before)
80
+ else
81
+ status, headers, body = app.call(env)
82
+ end
74
83
 
75
84
  if body.respond_to?(:to_ary)
76
85
  chunks = join_chunks(body.to_ary)
@@ -99,21 +108,12 @@ module Kino
99
108
  # delivery errors (they happen after app.call returned, so no
100
109
  # middleware can see them); its own failures are logged, not raised,
101
110
  # because nothing may escape this block and kill the worker.
102
- Native.log_error(error_log_line(e))
111
+ Log.exception(e, env)
103
112
  request.abort
104
113
  HookFire.fire(hooks&.on_error, "on_error", e, env)
105
114
  NOT_FUSED
106
115
  end
107
116
 
108
- # First frames only: the raise site is at the top, and Rails stacks
109
- # run hundreds of middleware frames deep. Hooks get the full exception.
110
- BACKTRACE_FRAMES = 12
111
-
112
- def error_log_line(error)
113
- ["#{error.class}: #{error.message}",
114
- *(error.backtrace || []).first(BACKTRACE_FRAMES)].join("\n ")
115
- end
116
-
117
117
  def deliver_streaming(request, status, headers, body, input)
118
118
  request.send_headers(status, headers)
119
119
  if body.respond_to?(:call) && !body.respond_to?(:each)
@@ -159,7 +159,6 @@ module Kino
159
159
  end
160
160
 
161
161
  private_class_method :handle_one, :process, :serve, :deliver_streaming,
162
- :join_chunks, :error_log_line, :fire_after_worker_boot,
163
- :fire_after_request_complete
162
+ :join_chunks, :fire_after_worker_boot, :fire_after_request_complete
164
163
  end
165
164
  end
@@ -7,5 +7,7 @@ module Kino
7
7
  # several bare procs. Any member may be nil. A Data instance is frozen,
8
8
  # so it is Ractor.shareable? exactly when its members are (nil, or a
9
9
  # Ractor.shareable_proc), letting it ride the ractor boundary like the app.
10
- WorkerHooks = Data.define(:on_error, :after_worker_boot, :after_request_complete)
10
+ # `access_timing` rides along: whether the worker measures the GC pause
11
+ # and allocations around each app call for the access log's breakdown.
12
+ WorkerHooks = Data.define(:on_error, :after_worker_boot, :after_request_complete, :access_timing)
11
13
  end
data/lib/kino.rb CHANGED
@@ -33,9 +33,20 @@ module Kino
33
33
  remaining = Native.sleep_chunk(remaining) while remaining.positive?
34
34
  nil
35
35
  end
36
+
37
+ # How many CPUs this process may actually use: the `workers` default.
38
+ # Unlike `Etc.nprocessors`, this honours a cgroup CPU quota (a container
39
+ # limited to 2 CPUs on a 64-core host gets 2, not 64) as well as the
40
+ # affinity mask; a fractional quota rounds up. Never below 1.
41
+ #
42
+ # @return [Integer]
43
+ def self.available_parallelism
44
+ Native.available_parallelism
45
+ end
36
46
  end
37
47
 
38
48
  require_relative "kino/cli"
49
+ require_relative "kino/log"
39
50
  require_relative "kino/logger"
40
51
  require_relative "kino/check"
41
52
  require_relative "kino/input"
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rackup
4
+ module Handler
5
+ # The Rack handler: lets any host that speaks the rackup protocol boot
6
+ # Kino, which is what `rackup -s kino` and `rails server -u kino` do.
7
+ # Loaded on demand by Rackup::Handler.get(:kino), so Rackup itself is
8
+ # already defined here; Kino is required only when the host calls in.
9
+ module Kino
10
+ # Host option name => Kino setting plus the coercion it needs: rackup
11
+ # hands `-O NAME=VALUE` values (and its own -p) over as strings.
12
+ OPTION_MAP = {
13
+ Host: [:bind, ->(value) { value.to_s }],
14
+ Port: [:port, ->(value) { Integer(value) }],
15
+ Workers: [:workers, ->(value) { Integer(value) }],
16
+ Threads: [:threads, ->(value) { Integer(value) }],
17
+ Mode: [:mode, ->(value) { value.to_sym }]
18
+ }.freeze
19
+ private_constant :OPTION_MAP
20
+
21
+ # Boot a server for `app` and block until it shuts down, the way the
22
+ # `kino` executable does (banner, INT/TERM drain, stats on USR1).
23
+ #
24
+ # @param app [#call] the Rack application the host built
25
+ # @param options [Hash] the host's options (see {.server_options})
26
+ # @yield [server] the built, not yet started server, for hosts that
27
+ # want a handle on it
28
+ # @return [::Kino::Server] the stopped server, after shutdown
29
+ def self.run(app, **options)
30
+ require "kino"
31
+ server = ::Kino::Server.new(app, **server_options(options))
32
+ yield server if block_given?
33
+ server.run
34
+ end
35
+
36
+ # The `-O NAME=VALUE` options `rackup -s kino --help` lists (rackup
37
+ # shows its own -o/-p in place of Host and Port).
38
+ # @return [Hash{String => String}]
39
+ def self.valid_options
40
+ {
41
+ "Host=HOST" => "Address to bind (default: 127.0.0.1)",
42
+ "Port=PORT" => "Port to listen on (default: 9292)",
43
+ "Workers=COUNT" => "Workers: ractors in :ractor mode, thread groups in :threaded (default: one per CPU)",
44
+ "Threads=COUNT" => "Threads per worker (default: 1 in :ractor, 3 in :threaded)",
45
+ "Mode=MODE" => "auto | ractor | threaded (default: auto)",
46
+ "Config=PATH" => "Kino config file (default: kino.rb, then config/kino.rb)"
47
+ }
48
+ end
49
+
50
+ # Translate host options into {::Kino::Server#initialize} kwargs.
51
+ # Precedence: options the user typed > the config file > defaults the
52
+ # host supplied (rackup's and Rails' own Host and Port) > Kino's
53
+ # defaults. Hosts that say which options were typed pass
54
+ # `user_supplied_options`; when that list is absent every option
55
+ # counts as typed. Keys outside OPTION_MAP (the host's bookkeeping:
56
+ # environment, pid, config, ...) are ignored.
57
+ #
58
+ # @param options [Hash{Symbol => Object}]
59
+ # @return [Hash{Symbol => Object}]
60
+ def self.server_options(options)
61
+ require "kino"
62
+ options = options.dup
63
+ host_defaults = {}
64
+ if (typed = options.delete(:user_supplied_options))
65
+ (options.keys - typed).each { |key| host_defaults[key] = options.delete(key) }
66
+ end
67
+
68
+ config = ::Kino::Configuration.new
69
+ path = options.delete(:Config) || host_defaults.delete(:Config) || ::Kino::Configuration.default_path
70
+ config.load_file(path) if path
71
+ translate(host_defaults).each { |key, value| config.set(key, value) unless config.set?(key) }
72
+ config.merge!(translate(options))
73
+ config.set(:port, ::Kino::Configuration::DEFAULT_SERVING_PORT) unless config.set?(:port)
74
+ config.server_options
75
+ end
76
+
77
+ def self.translate(options)
78
+ options.filter_map do |key, value|
79
+ setting, coerce = OPTION_MAP[key]
80
+ [setting, coerce.call(value)] if setting
81
+ end.to_h
82
+ end
83
+ private_class_method :translate
84
+ end
85
+
86
+ register :kino, Kino
87
+ end
88
+ end
data/sig/kino.rbs CHANGED
@@ -20,6 +20,9 @@ module Kino
20
20
  # High-resolution sleep on the OS clock with the GVL released.
21
21
  def self.sleep: (Numeric seconds) -> nil
22
22
 
23
+ # CPUs this process may use (affinity mask and cgroup quota); never below 1.
24
+ def self.available_parallelism: () -> Integer
25
+
23
26
  class Server
24
27
  attr_reader port: Integer?
25
28
  attr_reader control_port: Integer?
@@ -28,6 +31,16 @@ module Kino
28
31
 
29
32
  def tls?: () -> bool
30
33
 
34
+ # Whether the bind is a unix domain socket ("unix:///path/to.sock").
35
+ def unix?: () -> bool
36
+
37
+ # Where the server listens once started: http(s)://host:port or the
38
+ # unix:// socket path.
39
+ def url: () -> String
40
+
41
+ # Where the control plane listens once started, or nil when it is off.
42
+ def control_url: () -> String?
43
+
31
44
  # Settings precedence: explicit kwargs > config_file DSL > defaults.
32
45
  def initialize: (rack_app app, ?config_file: String?, **untyped options) -> void
33
46
 
@@ -42,9 +55,12 @@ module Kino
42
55
  # served, rejected, timeouts, respawns, lane_depths when lanes are on).
43
56
  def stats: () -> stats_hash
44
57
 
45
- # Production entry point: start, banner, signal traps, block until done.
58
+ # Production entry point: build the server and #run it.
46
59
  def self.run: (rack_app app, **untyped opts) -> Server
47
60
 
61
+ # Serve until shut down: start, banner, signal traps, block until done.
62
+ def run: () -> self
63
+
48
64
  # INT/TERM drain gracefully (second signal force-exits); USR1 prints stats.
49
65
  def self.trap_signals: (Server server) -> void
50
66
  end
@@ -53,6 +69,12 @@ module Kino
53
69
  DEFAULTS: Hash[Symbol, untyped]
54
70
  SETTINGS: Array[Symbol]
55
71
  SAMPLE_TEMPLATE: String
72
+ DEFAULT_PATHS: Array[String]
73
+ DEFAULT_SERVING_PORT: Integer
74
+
75
+ # The first of DEFAULT_PATHS (kino.rb, config/kino.rb) present in the
76
+ # working directory.
77
+ def self.default_path: () -> String?
56
78
 
57
79
  # The fully commented sample config (see `kino --init`).
58
80
  def self.sample: () -> String
@@ -168,6 +190,27 @@ module Kino
168
190
  def self.print_report: (rack_app app, ?io: IO) -> bool
169
191
  end
170
192
 
193
+ # Server log lines, `kino[<pid>] <source>: message`; safe inside worker
194
+ # ractors, so hooks may log through it.
195
+ module Log
196
+ FRAMES: Integer
197
+ WORKING_DIR: String
198
+
199
+ def self.info: (untyped message) -> void
200
+ def self.warn: (untyped message) -> void
201
+ def self.error: (untyped message) -> void
202
+
203
+ # The failed-request report: request line, error, and an app-first
204
+ # backtrace relative to the working directory.
205
+ def self.exception: (Exception error, Hash[String, untyped] env, ?status: Integer) -> void
206
+
207
+ # The `kino[<pid>] <source>:` tag.
208
+ def self.label: () -> String
209
+
210
+ # The ractor and/or thread name, or `main`.
211
+ def self.source: () -> String
212
+ end
213
+
171
214
  # A ::Logger writing through the native async sink.
172
215
  class Logger < ::Logger
173
216
  # path: a file (created/appended) or nil for stdout.
@@ -188,3 +231,21 @@ module Kino
188
231
  end
189
232
  end
190
233
  end
234
+
235
+ module Rackup
236
+ module Handler
237
+ # The Rack handler behind `rackup -s kino` and `rails server -u kino`.
238
+ module Kino
239
+ # Boot a server for app and block until shutdown; yields the built,
240
+ # not yet started server to hosts that want a handle on it.
241
+ def self.run: (::Kino::rack_app app, **untyped options) ?{ (::Kino::Server) -> void } -> ::Kino::Server
242
+
243
+ # The -O options rackup lists for this handler.
244
+ def self.valid_options: () -> Hash[String, String]
245
+
246
+ # Host options translated into Kino::Server kwargs, with precedence
247
+ # typed > config file > host defaults > Kino defaults.
248
+ def self.server_options: (Hash[Symbol, untyped] options) -> Hash[Symbol, untyped]
249
+ end
250
+ end
251
+ 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.3.0
4
+ version: 0.4.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-13 00:00:00.000000000 Z
11
+ date: 2026-08-22 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: rackup
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.2'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.2'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +164,7 @@ files:
150
164
  - lib/kino/hook_fire.rb
151
165
  - lib/kino/input.rb
152
166
  - lib/kino/kino.so
167
+ - lib/kino/log.rb
153
168
  - lib/kino/logger.rb
154
169
  - lib/kino/null_input.rb
155
170
  - lib/kino/quarantine_monitor.rb
@@ -160,6 +175,7 @@ files:
160
175
  - lib/kino/version.rb
161
176
  - lib/kino/worker.rb
162
177
  - lib/kino/worker_hooks.rb
178
+ - lib/rackup/handler/kino.rb
163
179
  - sig/kino.rbs
164
180
  homepage: https://github.com/yaroslav/kino
165
181
  licenses: