kino 0.2.0 → 0.3.0
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 +32 -0
- data/Cargo.lock +142 -139
- data/README.md +84 -3
- data/ext/kino/Cargo.toml +3 -3
- data/ext/kino/src/control.rs +718 -0
- data/ext/kino/src/lib.rs +12 -0
- data/ext/kino/src/mono.rs +26 -0
- data/ext/kino/src/queue.rs +7 -0
- data/ext/kino/src/registry.rs +155 -0
- data/ext/kino/src/request.rs +4 -0
- data/ext/kino/src/server.rs +116 -6
- data/lib/kino/cli.rb +5 -2
- data/lib/kino/configuration.rb +57 -0
- data/lib/kino/hook_fire.rb +23 -0
- data/lib/kino/quarantine_monitor.rb +70 -0
- data/lib/kino/ractor_supervisor.rb +59 -14
- data/lib/kino/server.rb +143 -21
- data/lib/kino/templates/kino.rb.tt +51 -0
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +36 -19
- data/lib/kino/worker_hooks.rb +11 -0
- data/lib/kino.rb +3 -0
- data/sig/kino.rbs +9 -0
- metadata +7 -2
data/README.md
CHANGED
|
@@ -183,7 +183,7 @@ bundle add kino # or: gem install kino (outside a bundle)
|
|
|
183
183
|
or put it in the `Gemfile` yourself:
|
|
184
184
|
|
|
185
185
|
```ruby
|
|
186
|
-
gem "kino"
|
|
186
|
+
gem "kino"
|
|
187
187
|
```
|
|
188
188
|
|
|
189
189
|
Then generate a config and serve:
|
|
@@ -230,6 +230,8 @@ server = Kino::Server.new(app,
|
|
|
230
230
|
max_body_size: 50 * 1024 * 1024, # bytes before a 413; nil = let a proxy handle it
|
|
231
231
|
on_error: ->(e, env) { ErrorTracker.capture(e) }, # after the client got its 500
|
|
232
232
|
shutdown_timeout: 30, # drain deadline
|
|
233
|
+
control_bind: "127.0.0.1:9293", # monitoring: /stats /metrics /ready /live; port 0 reads back via server.control_port
|
|
234
|
+
control_token: ENV["KINO_CONTROL_TOKEN"], # optional Bearer auth for /stats + /metrics
|
|
233
235
|
tls: { cert: "cert.pem", key: "key.pem" }, # file paths or inline PEM
|
|
234
236
|
)
|
|
235
237
|
server.start
|
|
@@ -320,6 +322,44 @@ after the client got its 500—the only place a tracker sees errors
|
|
|
320
322
|
raised while the response was being written (in `:ractor` mode, build
|
|
321
323
|
the handler with `Ractor.shareable_proc`).
|
|
322
324
|
|
|
325
|
+
## Lifecycle hooks
|
|
326
|
+
|
|
327
|
+
Kino fires four lifecycle hooks alongside `on_error`, split by firing context.
|
|
328
|
+
|
|
329
|
+
**Worker-context hooks** run inside the worker and are available to all workers:
|
|
330
|
+
- `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`.
|
|
331
|
+
- `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`.
|
|
332
|
+
|
|
333
|
+
**Main-context hooks** run on the main thread, outside workers, and are plain procs:
|
|
334
|
+
- `after_boot { }`: fires once after the worker pool is up. Wire readiness here—sd_notify, a "server ready" metric, and so on.
|
|
335
|
+
- `on_worker_exit { |worker_index, error| }`: fires when a worker exits, with its index and the crash cause (or nil on a clean exit).
|
|
336
|
+
|
|
337
|
+
`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.
|
|
338
|
+
|
|
339
|
+
A raising hook is logged and never kills a worker.
|
|
340
|
+
|
|
341
|
+
## Stuck-worker quarantine
|
|
342
|
+
|
|
343
|
+
`quarantine_timeout: seconds` (or `quarantine_timeout 60` in `kino.rb`)
|
|
344
|
+
quarantines a dispatch slot whose request has run longer than the deadline
|
|
345
|
+
and spawns a replacement worker to restore capacity—distinct from
|
|
346
|
+
`request_timeout`, which gives the client a 504 but leaves the slot
|
|
347
|
+
occupied. `quarantine_max` (default: the worker count in `:ractor` mode,
|
|
348
|
+
workers × threads in `:threaded`) caps the total number of replacement
|
|
349
|
+
events over the process lifetime—past it the monitor stops replacing and
|
|
350
|
+
the server runs at reduced capacity.
|
|
351
|
+
|
|
352
|
+
The wedged worker is never interrupted or force-killed, and its slot stays
|
|
353
|
+
quarantined for good. In `:threaded` mode, if the blocked thread
|
|
354
|
+
eventually returns, it keeps serving requests on that same slot—but the
|
|
355
|
+
slot itself stays flagged quarantined (busy_ms reported as 0) for the rest
|
|
356
|
+
of the process; in `:ractor` mode the wedged ractor (and its supervisor
|
|
357
|
+
thread) leaks until the process exits, since a wedged ractor cannot be
|
|
358
|
+
safely interrupted. Monitor quarantine activity via `server.stats`
|
|
359
|
+
(top-level `quarantined` count and per-slot `worker_status[].quarantined`
|
|
360
|
+
flag), `GET /stats` (same), and `GET /metrics` (`kino_quarantined_workers`
|
|
361
|
+
gauge and `kino_quarantine_replacements_total` counter).
|
|
362
|
+
|
|
323
363
|
## Stats
|
|
324
364
|
|
|
325
365
|
`server.stats` returns a live snapshot: the configuration plus counters
|
|
@@ -330,7 +370,7 @@ cost):
|
|
|
330
370
|
server.stats
|
|
331
371
|
# => {mode: :ractor, lanes: false, workers: 8, threads: 1, batch: 1,
|
|
332
372
|
# respawns: 0, queued: 0, in_flight: 2, served: 1041, rejected: 0,
|
|
333
|
-
# timeouts: 0}
|
|
373
|
+
# timeouts: 0, worker_status: [...]}
|
|
334
374
|
# plus lane_depths: [...] when lane dispatch is on
|
|
335
375
|
```
|
|
336
376
|
|
|
@@ -341,6 +381,39 @@ From the outside, `kill -USR1 <pid>` prints the same snapshot as one line
|
|
|
341
381
|
Kino stats: mode=:ractor lanes=false workers=8 threads=1 batch=1 respawns=0 queued=0 in_flight=2 served=1041 rejected=0 timeouts=0
|
|
342
382
|
```
|
|
343
383
|
|
|
384
|
+
For pull-based monitoring, `control_bind "127.0.0.1:9293"` (or a
|
|
385
|
+
`unix://` path) serves a read-only **control plane** from the native
|
|
386
|
+
layer on its own thread—it keeps answering even while every Ruby worker
|
|
387
|
+
is busy or stuck, and reports `draining` through a graceful shutdown:
|
|
388
|
+
|
|
389
|
+
- `GET /stats`—the same snapshot as `server.stats`, as JSON (plus
|
|
390
|
+
`state` and `version`).
|
|
391
|
+
- `GET /metrics`—Prometheus text format (`kino_requests_served_total`,
|
|
392
|
+
`kino_queue_depth`, `kino_ready`, …).
|
|
393
|
+
|
|
394
|
+
Both `/stats` and `/metrics` also break the counters down per dispatch
|
|
395
|
+
slot: `/stats` carries a `worker_status` array (`index`, `served`,
|
|
396
|
+
`in_flight`, `busy_ms`) and `/metrics` emits `kino_worker_*{worker="N"}`
|
|
397
|
+
series, one entry per execution slot (`workers × threads`)—a crashed
|
|
398
|
+
worker's slot is never reused, so it stays in the list with its counters
|
|
399
|
+
frozen where they stopped, meaning the array (and its `worker="N"` metric
|
|
400
|
+
series) grows by one across every respawn. `busy_ms` is how long the
|
|
401
|
+
slot's current request has been running (0 when idle), so a single slot
|
|
402
|
+
climbing while the rest sit at 0 is your stuck worker.
|
|
403
|
+
|
|
404
|
+
The `/stats` response and `server.stats` carry `queue_time` (count and
|
|
405
|
+
summed seconds), and `/metrics` exposes `kino_request_queue_seconds`—a
|
|
406
|
+
Prometheus histogram of queue-wait time, the worker-saturation signal.
|
|
407
|
+
Counts admitted requests only; a 503 after queue wait goes to `rejected`,
|
|
408
|
+
not `queue_time`.
|
|
409
|
+
|
|
410
|
+
- `GET /ready`—`200` when serving, `503` while booting or draining:
|
|
411
|
+
wire it to your load balancer or Kubernetes readiness probe.
|
|
412
|
+
- `GET /live`—`200` whenever the process is alive: the liveness probe.
|
|
413
|
+
|
|
414
|
+
`control_token "..."` puts `/stats` and `/metrics` behind
|
|
415
|
+
`Authorization: Bearer`; the probes stay open.
|
|
416
|
+
|
|
344
417
|
## Logging
|
|
345
418
|
|
|
346
419
|
With one log line per request, `Kino::Logger` sustained **2.4× the
|
|
@@ -432,9 +505,17 @@ bundle exec rake # compile, Rust tests, specs, RBS, lint
|
|
|
432
505
|
RB_SYS_CARGO_PROFILE=dev bundle exec rake compile # fast dev rebuilds
|
|
433
506
|
```
|
|
434
507
|
|
|
508
|
+
## Acknowledgements
|
|
509
|
+
|
|
510
|
+
Thanks to [Mat Sadler](https://github.com/matsadler) for [magnus](https://github.com/matsadler/magnus).
|
|
511
|
+
|
|
512
|
+
For ractors, thanks to [Koichi Sasada](https://github.com/ko1), [John Hawthorn](https://github.com/jhawthorn), [Jean Boussier](https://github.com/byroot), [Luke Gruber](https://github.com/luke-gruber), and other Ruby core contributors.
|
|
513
|
+
|
|
514
|
+
For the Rust network stack, thanks to [Sean McArthur](https://github.com/seanmonstar) for [hyper](https://github.com/hyperium/hyper), and to [Carl Lerche](https://github.com/carllerche), [Alice Ryhl](https://github.com/Darksonn), and the other [Tokio](https://github.com/tokio-rs/tokio) maintainers for the runtime underneath it. Thanks to [Joshua Barretto](https://github.com/zesterer) for [flume](https://github.com/zesterer/flume)—its channels carry every request between the network side and the workers.
|
|
515
|
+
|
|
435
516
|
## Assisted by
|
|
436
517
|
|
|
437
|
-
Claude Code (
|
|
518
|
+
Claude Code (Fable 5, Opus 4.8).
|
|
438
519
|
|
|
439
520
|
## Contributing
|
|
440
521
|
|
data/ext/kino/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "kino"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
authors = ["Yaroslav Markin <yaroslav@markin.net>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -14,11 +14,11 @@ crate-type = ["cdylib", "rlib"]
|
|
|
14
14
|
[dependencies]
|
|
15
15
|
magnus = { version = "0.8.2", features = ["rb-sys"] }
|
|
16
16
|
rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
|
|
17
|
-
flume = "0.
|
|
17
|
+
flume = "0.12"
|
|
18
18
|
parking_lot = "0.12"
|
|
19
19
|
ahash = "0.8"
|
|
20
20
|
smallvec = "1"
|
|
21
|
-
lru = "0.
|
|
21
|
+
lru = "0.18"
|
|
22
22
|
mimalloc = { version = "0.1", default-features = false }
|
|
23
23
|
tokio = { version = "1.45", features = ["rt-multi-thread", "net", "time", "sync", "io-util", "macros"] }
|
|
24
24
|
hyper = { version = "1.6", features = ["http1", "server"] }
|