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 +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +56 -14
- data/exe/kino +0 -4
- data/lib/kino/cli.rb +20 -9
- data/lib/kino/configuration.rb +20 -5
- data/lib/kino/errors_stream.rb +4 -3
- data/lib/kino/hook_fire.rb +3 -4
- data/lib/kino/kino.so +0 -0
- data/lib/kino/log.rb +104 -0
- data/lib/kino/quarantine_monitor.rb +7 -4
- data/lib/kino/ractor_supervisor.rb +8 -3
- data/lib/kino/server.rb +60 -15
- data/lib/kino/templates/kino.rb.tt +11 -5
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +12 -13
- data/lib/kino/worker_hooks.rb +3 -1
- data/lib/kino.rb +11 -0
- data/lib/rackup/handler/kino.rb +88 -0
- data/sig/kino.rbs +62 -1
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 145273ae455b0822a24794c25d449376b3ba5122f3f275c00b4a959fdafd5183
|
|
4
|
+
data.tar.gz: c20ae1c2f111a4f6329ea25a4d9c9a0579dfa1b77765ff11e35572f8e32925ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
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.
|
|
259
|
-
|
|
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>`
|
|
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
|
-
|
|
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`):
|
|
426
|
-
including the 503s that never reach your app.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
|
495
|
-
`examples/rails-hello`. Ractor-mode Rails
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
puts dim("-
|
|
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
|
|
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,
|
|
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
|
data/lib/kino/configuration.rb
CHANGED
|
@@ -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
|
|
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] ||=
|
|
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
|
|
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.
|
data/lib/kino/errors_stream.rb
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Kino
|
|
4
4
|
# @private
|
|
5
|
-
# rack.errors: stateless writer into the
|
|
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
|
-
|
|
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
|
-
|
|
16
|
+
Log.error(message.chomp)
|
|
16
17
|
message.bytesize
|
|
17
18
|
end
|
|
18
19
|
|
data/lib/kino/hook_fire.rb
CHANGED
|
@@ -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
|
-
#
|
|
8
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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:
|
|
199
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
CLI.action!(
|
|
251
|
+
start
|
|
252
|
+
CLI.action!(self)
|
|
210
253
|
CLI.fin_at_exit
|
|
211
|
-
trap_signals(
|
|
212
|
-
|
|
213
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
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
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
|
-
|
|
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
|
-
|
|
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, :
|
|
163
|
-
:fire_after_request_complete
|
|
162
|
+
:join_chunks, :fire_after_worker_boot, :fire_after_request_complete
|
|
164
163
|
end
|
|
165
164
|
end
|
data/lib/kino/worker_hooks.rb
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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.
|
|
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-
|
|
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:
|