kino 0.3.0 → 0.4.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 +45 -0
- data/Cargo.lock +133 -4
- data/README.md +56 -14
- data/exe/kino +0 -4
- data/ext/kino/Cargo.toml +4 -2
- data/ext/kino/src/access_log.rs +245 -0
- data/ext/kino/src/control.rs +12 -29
- data/ext/kino/src/cpus.rs +37 -0
- data/ext/kino/src/lib.rs +10 -1
- data/ext/kino/src/listen.rs +134 -0
- data/ext/kino/src/log.rs +144 -0
- data/ext/kino/src/queue.rs +7 -3
- data/ext/kino/src/registry.rs +3 -0
- data/ext/kino/src/request.rs +48 -2
- data/ext/kino/src/server.rs +114 -48
- data/ext/kino/src/style.rs +42 -39
- 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/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 +21 -1
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
|