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 +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +131 -16
- data/exe/kino +0 -4
- data/lib/kino/cli.rb +23 -9
- data/lib/kino/configuration.rb +77 -5
- data/lib/kino/errors_stream.rb +4 -3
- data/lib/kino/hook_fire.rb +22 -0
- data/lib/kino/kino.so +0 -0
- data/lib/kino/log.rb +104 -0
- data/lib/kino/quarantine_monitor.rb +73 -0
- data/lib/kino/ractor_supervisor.rb +66 -16
- data/lib/kino/server.rb +201 -34
- data/lib/kino/templates/kino.rb.tt +62 -5
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +46 -30
- data/lib/kino/worker_hooks.rb +13 -0
- data/lib/kino.rb +14 -0
- data/lib/rackup/handler/kino.rb +88 -0
- data/sig/kino.rbs +71 -1
- metadata +21 -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,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:
|
|
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.
|
|
257
|
-
|
|
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>`
|
|
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
|
-
|
|
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`):
|
|
353
|
-
including the 503s that never reach your app.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
|
422
|
-
`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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
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
|
|
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
|
|
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,
|
|
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
|
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,
|
|
@@ -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] ||=
|
|
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
|
|
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
|
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
|
|
|
@@ -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
|