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.
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 { run }
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
- Native.log_error("quarantine monitor crashed: #{e.class}: #{e.message}")
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
- Native.log_error("quarantine tick error: #{e.class}: #{e.message}")
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
- Native.log_error("quarantine at cap (#{@max}); serving at reduced capacity")
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
- Native.log_error("worker ractor #{index} crashed (#{cause.class}: #{cause.message}); respawning")
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
- ractor = Ractor.new(@server_id, worker_ids, @app, @batch, @hooks) do |server_id, ids, app, batch, hooks|
133
- ids.map do |id|
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: start, print the banner, trap INT/TERM for
199
- # graceful shutdown (second signal force-exits), block until done.
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
- server = new(app, **opts)
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
- server.start
209
- CLI.action!(server)
251
+ start
252
+ CLI.action!(self)
210
253
  CLI.fin_at_exit
211
- trap_signals(server)
212
- server.wait
213
- server
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 { $stdout.puts Kino::CLI.stats_line(server.stats) }
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
- $stderr.write("Kino: draining (signal again to force exit)\n")
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
- Native.log_error("shutdown deadline passed with stuck ractor workers") unless @supervisor.done?
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 "Kino: app is not Ractor-shareable; falling back to mode: :threaded"
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 "Kino: #{name} hook is not Ractor-shareable; falling back to mode: :threaded"
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: one per CPU core.
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
- # Print one line per request to stdout, colored by status on a
78
- # terminal. This is the server's view: it includes requests your app
79
- # never saw, such as 503s. Recommended in development.
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Kino
4
4
  # The gem version (single source of truth; ext/kino/Cargo.toml syncs).
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
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
- status, headers, body = app.call(env)
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
- Native.log_error(error_log_line(e))
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, :error_log_line, :fire_after_worker_boot,
163
- :fire_after_request_complete
162
+ :join_chunks, :fire_after_worker_boot, :fire_after_request_complete
164
163
  end
165
164
  end
@@ -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
- WorkerHooks = Data.define(:on_error, :after_worker_boot, :after_request_complete)
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: start, banner, signal traps, block until done.
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