kino 0.2.1-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: d664928d6dfad3b60c6fdcbd27c2dffc008a9485a224bda3b7179f033c4c7cf5
4
- data.tar.gz: d52e42477237ff7e65ea5f8752a63c6ac1d1f20cfa3f9189ad3a80a4d5e51f69
3
+ metadata.gz: 145273ae455b0822a24794c25d449376b3ba5122f3f275c00b4a959fdafd5183
4
+ data.tar.gz: c20ae1c2f111a4f6329ea25a4d9c9a0579dfa1b77765ff11e35572f8e32925ca
5
5
  SHA512:
6
- metadata.gz: e0a0a7ac4cfc422143b11938423cd1b9022e4e065bf131548b485a95d1cd88042798f778b5b87f3fc6a51a12814cf564be8bce8d4ed61366e68c6133e775a3e1
7
- data.tar.gz: 1d52a5e4715075b324cd9997e525be33c4edf69c4fe0547dcbf8633228783b3f7414699a88a70caeab2d7f1122d80283f4ab7c5cd7f45669bd67b3dae190a12c
6
+ metadata.gz: ed2ef3b3d79817b67da62e8271f0a420d4ee0c8976780d97e6e91ba15cddff2e1d34e830d41a8c4c8c63d65fecbeab32bec8d8568777b399c684cdfce00624f0
7
+ data.tar.gz: 6d695d16998c2bdb9fe44ac709a311990c53c88ae980f136757db926ba370e5a8e379fdd098f675beae4fb126a0ce7d663b36b0a602d6933b1c2b6d3f6cdc02b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,75 @@
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
+
46
+ ## [0.3.0] - 2026-08-13
47
+
48
+ - Queue-time histogram: `/metrics` exposes `kino_request_queue_seconds`, a
49
+ histogram of how long each request waited for a free worker (the
50
+ saturation signal), and `server.stats`/`/stats` gain `queue_time`
51
+ (count and summed seconds). Measured internally with a monotonic clock,
52
+ so it needs no proxy header and is immune to clock skew.
53
+ - Lifecycle hooks: `after_boot`, `after_worker_boot`,
54
+ `after_request_complete`, and `on_worker_exit` join `on_error`, so apps
55
+ can wire their own metrics, readiness, and error tracking. The
56
+ worker-context hooks must be Ractor-shareable in `:ractor` mode; a raising
57
+ hook is logged and never kills a worker.
58
+ - Stuck-worker quarantine: past `quarantine_timeout`, a wedged dispatch
59
+ slot is quarantined and a replacement worker is spawned to restore
60
+ capacity (capped by `quarantine_max`), surfaced via `/stats`, `/metrics`,
61
+ and `server.stats`. The wedged worker is never force-killed.
62
+ - Control plane: a read-only monitoring listener (`control_bind`,
63
+ optional `control_token`) serving live stats as JSON at `/stats`,
64
+ Prometheus metrics at `/metrics`, and `/ready`/`/live` probes, answered
65
+ from the native layer on a dedicated thread so it stays responsive
66
+ while workers are busy, stuck, or draining.
67
+ - Per-worker stats: `/stats`, `/metrics`, and `server.stats` now break the
68
+ counters down per dispatch slot (served, in-flight, and `busy_ms`, the
69
+ age of the slot's current request), so a stuck slot is visible
70
+ individually.
71
+ - Update Rust and Ruby dependencies.
72
+
1
73
  ## [0.2.1] - 2026-07-27
2
74
 
3
75
  - Update Rust dependencies for Kino.
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
@@ -230,6 +236,8 @@ server = Kino::Server.new(app,
230
236
  max_body_size: 50 * 1024 * 1024, # bytes before a 413; nil = let a proxy handle it
231
237
  on_error: ->(e, env) { ErrorTracker.capture(e) }, # after the client got its 500
232
238
  shutdown_timeout: 30, # drain deadline
239
+ control_bind: "127.0.0.1:9293", # monitoring: /stats /metrics /ready /live; port 0 reads back via server.control_port
240
+ control_token: ENV["KINO_CONTROL_TOKEN"], # optional Bearer auth for /stats + /metrics
233
241
  tls: { cert: "cert.pem", key: "key.pem" }, # file paths or inline PEM
234
242
  )
235
243
  server.start
@@ -253,8 +261,10 @@ server.shutdown # graceful: drain → deadline → abort straggler
253
261
 
254
262
  ## Config file and CLI
255
263
 
256
- Settings can live in a Puma-style Ruby DSL file. Precedence: explicit
257
- 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.
258
268
 
259
269
  ```ruby
260
270
  # kino.rb
@@ -320,6 +330,44 @@ after the client got its 500—the only place a tracker sees errors
320
330
  raised while the response was being written (in `:ractor` mode, build
321
331
  the handler with `Ractor.shareable_proc`).
322
332
 
333
+ ## Lifecycle hooks
334
+
335
+ Kino fires four lifecycle hooks alongside `on_error`, split by firing context.
336
+
337
+ **Worker-context hooks** run inside the worker and are available to all workers:
338
+ - `after_worker_boot { |worker_id| }`: runs once before the worker begins serving, with its slot id. In `:ractor` mode it runs inside the worker ractor and must be `Ractor.shareable_proc`.
339
+ - `after_request_complete { |env, status| }`: fires inside the worker after each successful response. This is the hot path—leave it unset for zero cost. In `:ractor` mode it must be `Ractor.shareable_proc`.
340
+
341
+ **Main-context hooks** run on the main thread, outside workers, and are plain procs:
342
+ - `after_boot { }`: fires once after the worker pool is up. Wire readiness here—sd_notify, a "server ready" metric, and so on.
343
+ - `on_worker_exit { |worker_index, error| }`: fires when a worker exits, with its index and the crash cause (or nil on a clean exit).
344
+
345
+ `after_worker_boot`'s argument is the worker's slot id, while in `:ractor` mode `on_worker_exit`'s argument identifies the exited ractor (`0`..`workers - 1`)—a different number space—so don't correlate boot and exit by that number in `:ractor` mode.
346
+
347
+ A raising hook is logged and never kills a worker.
348
+
349
+ ## Stuck-worker quarantine
350
+
351
+ `quarantine_timeout: seconds` (or `quarantine_timeout 60` in `kino.rb`)
352
+ quarantines a dispatch slot whose request has run longer than the deadline
353
+ and spawns a replacement worker to restore capacity—distinct from
354
+ `request_timeout`, which gives the client a 504 but leaves the slot
355
+ occupied. `quarantine_max` (default: the worker count in `:ractor` mode,
356
+ workers × threads in `:threaded`) caps the total number of replacement
357
+ events over the process lifetime—past it the monitor stops replacing and
358
+ the server runs at reduced capacity.
359
+
360
+ The wedged worker is never interrupted or force-killed, and its slot stays
361
+ quarantined for good. In `:threaded` mode, if the blocked thread
362
+ eventually returns, it keeps serving requests on that same slot—but the
363
+ slot itself stays flagged quarantined (busy_ms reported as 0) for the rest
364
+ of the process; in `:ractor` mode the wedged ractor (and its supervisor
365
+ thread) leaks until the process exits, since a wedged ractor cannot be
366
+ safely interrupted. Monitor quarantine activity via `server.stats`
367
+ (top-level `quarantined` count and per-slot `worker_status[].quarantined`
368
+ flag), `GET /stats` (same), and `GET /metrics` (`kino_quarantined_workers`
369
+ gauge and `kino_quarantine_replacements_total` counter).
370
+
323
371
  ## Stats
324
372
 
325
373
  `server.stats` returns a live snapshot: the configuration plus counters
@@ -330,17 +378,50 @@ cost):
330
378
  server.stats
331
379
  # => {mode: :ractor, lanes: false, workers: 8, threads: 1, batch: 1,
332
380
  # respawns: 0, queued: 0, in_flight: 2, served: 1041, rejected: 0,
333
- # timeouts: 0}
381
+ # timeouts: 0, worker_status: [...]}
334
382
  # plus lane_depths: [...] when lane dispatch is on
335
383
  ```
336
384
 
337
- 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
338
386
  (pair it with `pidfile` to find the pid):
339
387
 
340
388
  ```
341
- 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
342
390
  ```
343
391
 
392
+ For pull-based monitoring, `control_bind "127.0.0.1:9293"` (or a
393
+ `unix://` path) serves a read-only **control plane** from the native
394
+ layer on its own thread—it keeps answering even while every Ruby worker
395
+ is busy or stuck, and reports `draining` through a graceful shutdown:
396
+
397
+ - `GET /stats`—the same snapshot as `server.stats`, as JSON (plus
398
+ `state` and `version`).
399
+ - `GET /metrics`—Prometheus text format (`kino_requests_served_total`,
400
+ `kino_queue_depth`, `kino_ready`, …).
401
+
402
+ Both `/stats` and `/metrics` also break the counters down per dispatch
403
+ slot: `/stats` carries a `worker_status` array (`index`, `served`,
404
+ `in_flight`, `busy_ms`) and `/metrics` emits `kino_worker_*{worker="N"}`
405
+ series, one entry per execution slot (`workers × threads`)—a crashed
406
+ worker's slot is never reused, so it stays in the list with its counters
407
+ frozen where they stopped, meaning the array (and its `worker="N"` metric
408
+ series) grows by one across every respawn. `busy_ms` is how long the
409
+ slot's current request has been running (0 when idle), so a single slot
410
+ climbing while the rest sit at 0 is your stuck worker.
411
+
412
+ The `/stats` response and `server.stats` carry `queue_time` (count and
413
+ summed seconds), and `/metrics` exposes `kino_request_queue_seconds`—a
414
+ Prometheus histogram of queue-wait time, the worker-saturation signal.
415
+ Counts admitted requests only; a 503 after queue wait goes to `rejected`,
416
+ not `queue_time`.
417
+
418
+ - `GET /ready`—`200` when serving, `503` while booting or draining:
419
+ wire it to your load balancer or Kubernetes readiness probe.
420
+ - `GET /live`—`200` whenever the process is alive: the liveness probe.
421
+
422
+ `control_token "..."` puts `/stats` and `/metrics` behind
423
+ `Authorization: Bearer`; the probes stay open.
424
+
344
425
  ## Logging
345
426
 
346
427
  With one log line per request, `Kino::Logger` sustained **2.4× the
@@ -349,16 +430,28 @@ box). There are two native pieces. Both write through a lock-free
349
430
  channel to a Rust flusher thread, so request threads never take a log
350
431
  mutex and never make a write syscall:
351
432
 
352
- - **Access log** (`log_requests true`): one line per request to stdout,
353
- including the 503s that never reach your app. Recommended in
354
- development; cheap enough for production. On color terminals the
355
- lines are tinted by status class: 2xx green, 3xx yellow, 4xx maroon,
356
- 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:
357
443
 
358
444
  ```
359
- 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)
360
447
  ```
361
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
+
362
455
  - **`Kino::Logger`**: a `::Logger` over the same async sink, for your
363
456
  app's own logging (`Kino::Logger.new("log/production.log")`, or no
364
457
  argument for stdout). The raw IO-like device is `Kino::Logger::Device`,
@@ -400,6 +493,27 @@ lines/s), the sink drops lines instead of blocking request threads.
400
493
  These trade-offs are measured in
401
494
  [doc/benchmarks.md](doc/benchmarks.md#logging-costs).
402
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
+
403
517
  ## Timer waits
404
518
 
405
519
  `Kino.sleep(seconds)` is a high-resolution sleep on the OS clock with
@@ -418,8 +532,9 @@ optional in Rack 3.
418
532
 
419
533
  ## Rails
420
534
 
421
- Rails (edge) runs on Kino today in `:threaded` mode; see
422
- `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
423
538
  blockers, the `Ruby::Box` findings, and what would unlock it are written
424
539
  up in [doc/rails-on-ractors.md](doc/rails-on-ractors.md). The example
425
540
  ships a probe script that re-tests against whatever Rails you bundle.
@@ -442,7 +557,7 @@ For the Rust network stack, thanks to [Sean McArthur](https://github.com/seanmon
442
557
 
443
558
  ## Assisted by
444
559
 
445
- Claude Code (Mythos, Opus).
560
+ Claude Code (Fable 5, Opus 4.8).
446
561
 
447
562
  ## Contributing
448
563
 
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,11 +100,14 @@ module Kino
100
100
  end.join
101
101
  end
102
102
 
103
- # One-line stats dump (the SIGUSR1 handler's output).
103
+ # One-line stats dump (what the SIGUSR1 handler logs). Excludes
104
+ # worker_status: it's an array with one entry per execution slot, and
105
+ # printing it inline would break the one-line contract (see /stats for
106
+ # per-worker detail).
104
107
  # @param stats [Hash{Symbol => Object}] see {Kino::Server#stats}
105
108
  # @return [String]
106
109
  def stats_line(stats)
107
- dim("Kino stats: #{stats.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}")
110
+ "stats #{stats.except(:worker_status).map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}"
108
111
  end
109
112
 
110
113
  # The two banner halves around Server#start: credits before, the ready
@@ -116,15 +119,27 @@ module Kino
116
119
  puts dim("\nKino #{VERSION} presents:")
117
120
  end
118
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.
119
124
  # @param server [Kino::Server] a started server
120
125
  # @return [void]
121
126
  def action!(server)
122
- puts dim("- mode: #{server.mode}")
123
- 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
124
134
  puts dim("- Ctrl-C to drain and stop")
125
135
  puts "\n#{bold("Action!")}\n\n"
126
136
  end
127
137
 
138
+ # "1 worker", "8 workers".
139
+ def count(number, noun)
140
+ "#{number} #{noun}#{"s" unless number == 1}"
141
+ end
142
+
128
143
  # Roll credits when the process ends: normal exit or crash (at_exit
129
144
  # also runs after an uncaught exception; only a force-exit skips it).
130
145
  # @return [void]
@@ -197,7 +212,7 @@ module Kino
197
212
  def option_parser(options)
198
213
  OptionParser.new do |opts|
199
214
  opts.banner = "Usage: kino [options] [rackup file (default: config.ru)]"
200
- 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 }
201
216
  opts.on("--init [PATH]", "Write a commented sample config (default: kino.rb) and exit") do |v|
202
217
  options[:init_path] = v || "kino.rb"
203
218
  end
@@ -233,14 +248,13 @@ module Kino
233
248
  require "kino"
234
249
  require "rack"
235
250
 
236
- config_file = options[:config_file]
237
- config_file ||= ("kino.rb" if File.exist?("kino.rb"))
251
+ config_file = options[:config_file] || Configuration.default_path
238
252
 
239
253
  config = Configuration.new
240
254
  config.load_file(config_file) if config_file
241
255
  config.merge!(options[:overrides])
242
256
  # Default port 9292 when neither the file nor a flag chose one.
243
- config.set(:port, 9292) unless config.set?(:port)
257
+ config.set(:port, Configuration::DEFAULT_SERVING_PORT) unless config.set?(:port)
244
258
  config
245
259
  end
246
260
 
@@ -249,6 +263,6 @@ module Kino
249
263
  end
250
264
 
251
265
  private_class_method :print_help, :option_parser, :write_sample,
252
- :resolve_config, :serve
266
+ :resolve_config, :serve, :count
253
267
  end
254
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,
@@ -23,11 +21,19 @@ module Kino
23
21
  lanes: false,
24
22
  log_requests: false,
25
23
  on_error: nil,
24
+ after_boot: nil,
25
+ after_worker_boot: nil,
26
+ after_request_complete: nil,
27
+ on_worker_exit: nil,
26
28
  shutdown_timeout: 30,
27
29
  tokio_threads: nil,
28
30
  tls: nil,
29
31
  environment: nil,
30
32
  pidfile: nil,
33
+ control_bind: nil,
34
+ control_token: nil,
35
+ quarantine_timeout: nil,
36
+ quarantine_max: nil,
31
37
  rackup: nil
32
38
  }.freeze
33
39
 
@@ -37,6 +43,21 @@ module Kino
37
43
  # Source template for {.sample}.
38
44
  SAMPLE_TEMPLATE = File.expand_path("templates/kino.rb.tt", __dir__)
39
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
+
40
61
  # The fully-commented sample config (see `kino --init`).
41
62
  # @return [String]
42
63
  def self.sample
@@ -105,7 +126,7 @@ module Kino
105
126
  # @return [Hash{Symbol => Object}] every setting, defaults filled in
106
127
  def to_h
107
128
  SETTINGS.to_h { |key| [key, self[key]] }.tap do |h|
108
- h[:workers] ||= Etc.nprocessors
129
+ h[:workers] ||= Kino.available_parallelism
109
130
  end
110
131
  end
111
132
 
@@ -138,7 +159,9 @@ module Kino
138
159
  @config = config
139
160
  end
140
161
 
141
- # 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).
142
165
  def bind(host) = @config.set(:bind, host)
143
166
 
144
167
  # Port to listen on; 0 picks an ephemeral port.
@@ -185,6 +208,24 @@ module Kino
185
208
  # or a block. Must be Ractor-shareable in :ractor mode.
186
209
  def on_error(handler = nil, &block) = @config.set(:on_error, handler || block)
187
210
 
211
+ # Called once on the main thread after the worker pool is up. The
212
+ # readiness seam (wire sd_notify or a "server ready" metric here).
213
+ def after_boot(handler = nil, &block) = @config.set(:after_boot, handler || block)
214
+
215
+ # Called once inside each worker (a ractor in :ractor mode) before it
216
+ # serves, with the worker's slot id. Must be Ractor-shareable in
217
+ # :ractor mode (build it with Ractor.shareable_proc).
218
+ def after_worker_boot(handler = nil, &block) = @config.set(:after_worker_boot, handler || block)
219
+
220
+ # Called inside the worker after each successful response with
221
+ # (env, status). Hot path: leave unset for zero cost. Must be
222
+ # Ractor-shareable in :ractor mode.
223
+ def after_request_complete(handler = nil, &block) = @config.set(:after_request_complete, handler || block)
224
+
225
+ # Called on the main thread when a worker exits, with (worker_index,
226
+ # error_or_nil). error is the crash cause, or nil on a clean exit.
227
+ def on_worker_exit(handler = nil, &block) = @config.set(:on_worker_exit, handler || block)
228
+
188
229
  # Graceful-shutdown drain deadline in seconds.
189
230
  def shutdown_timeout(seconds) = @config.set(:shutdown_timeout, seconds)
190
231
 
@@ -200,6 +241,37 @@ module Kino
200
241
  # Write the master PID here on start.
201
242
  def pidfile(path) = @config.set(:pidfile, path.to_s)
202
243
 
244
+ # Serve the read-only control plane (live stats as JSON at /stats,
245
+ # Prometheus text at /metrics, /ready and /live probes) on this
246
+ # address: "host:port" or "unix://path". Off unless set.
247
+ def control_bind(addr) = @config.set(:control_bind, addr.to_s)
248
+
249
+ # When set, /stats and /metrics require "Authorization: Bearer <token>".
250
+ # The probes stay open; they carry no data.
251
+ def control_token(token) = @config.set(:control_token, token.to_s)
252
+
253
+ # Quarantine a dispatch slot whose current request has run longer
254
+ # than this many seconds, spawning a replacement to restore capacity.
255
+ # Off unless set. Set it above your slowest legitimate endpoint (and
256
+ # typically above request_timeout).
257
+ def quarantine_timeout(seconds)
258
+ seconds &&= Float(seconds)
259
+ if seconds && seconds <= 0
260
+ raise ArgumentError, "quarantine_timeout must be greater than 0 (got #{seconds})"
261
+ end
262
+ @config.set(:quarantine_timeout, seconds)
263
+ end
264
+
265
+ # Cap on the total number of replacement events over the process
266
+ # lifetime. Past the cap the monitor stops replacing and the server
267
+ # runs at reduced capacity. Default: the worker count in :ractor
268
+ # mode, workers x threads in :threaded.
269
+ def quarantine_max(count)
270
+ count = Integer(count)
271
+ raise ArgumentError, "quarantine_max must be >= 1 (got #{count})" if count < 1
272
+ @config.set(:quarantine_max, count)
273
+ end
274
+
203
275
  # Rackup file the `kino` CLI loads (positional argument wins).
204
276
  def rackup(path) = @config.set(:rackup, path.to_s)
205
277
  end
@@ -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
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kino
4
+ # @private
5
+ # Fires a lifecycle hook and turns a raise into a logged line instead of
6
+ # letting it escape. Stateless and touches only its arguments plus
7
+ # Kino::Log (safe inside worker ractors), so it is safe to call from
8
+ # worker context: no main-ractor state is captured.
9
+ module HookFire
10
+ module_function
11
+
12
+ def fire(hook, name, *args)
13
+ return unless hook
14
+
15
+ begin
16
+ hook.call(*args)
17
+ rescue => e
18
+ Log.error("#{name} hook raised #{e.class}: #{e.message}")
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/kino/kino.so CHANGED
Binary file