kino 0.1.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 +7 -0
- data/.yardopts +14 -0
- data/CHANGELOG.md +54 -0
- data/LICENSE.txt +21 -0
- data/README.md +384 -0
- data/doc/README.md +6 -0
- data/doc/architecture.md +161 -0
- data/doc/benchmarks.md +321 -0
- data/doc/rails-on-ractors.md +50 -0
- data/doc/why-kino.md +91 -0
- data/exe/kino +26 -0
- data/lib/kino/check.rb +199 -0
- data/lib/kino/cli.rb +254 -0
- data/lib/kino/configuration.rb +190 -0
- data/lib/kino/errors_stream.rb +25 -0
- data/lib/kino/input.rb +77 -0
- data/lib/kino/kino.so +0 -0
- data/lib/kino/logger.rb +56 -0
- data/lib/kino/null_input.rb +37 -0
- data/lib/kino/ractor_supervisor.rb +103 -0
- data/lib/kino/server.rb +271 -0
- data/lib/kino/stream.rb +61 -0
- data/lib/kino/templates/kino.rb.tt +141 -0
- data/lib/kino/version.rb +6 -0
- data/lib/kino/worker.rb +124 -0
- data/lib/kino.rb +53 -0
- data/sig/kino.rbs +178 -0
- metadata +193 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Kino configuration.
|
|
4
|
+
# Generated by `kino --init`.
|
|
5
|
+
#
|
|
6
|
+
# Every setting below is shown with its default value, commented out:
|
|
7
|
+
# the file is a valid no-op until you uncomment something. Precedence:
|
|
8
|
+
# explicit Server.new kwargs / CLI flags > this file > built-in defaults.
|
|
9
|
+
|
|
10
|
+
## Network
|
|
11
|
+
|
|
12
|
+
# Address to listen on. Use "0.0.0.0" to accept non-local connections.
|
|
13
|
+
# bind "127.0.0.1"
|
|
14
|
+
|
|
15
|
+
# Port to listen on. 0 picks an ephemeral port (readable via server.port).
|
|
16
|
+
# The `kino` CLI defaults this to 9292 when nothing else sets it.
|
|
17
|
+
# port 9292
|
|
18
|
+
|
|
19
|
+
# TLS termination (rustls, in Rust; never blocks a Ruby thread).
|
|
20
|
+
# Values are file paths or inline PEM strings. ALPN is http/1.1.
|
|
21
|
+
# tls cert: "config/certs/server.pem", key: "config/certs/server.key"
|
|
22
|
+
|
|
23
|
+
## Topology
|
|
24
|
+
|
|
25
|
+
# Puma-style two-level topology: `workers` × `threads`.
|
|
26
|
+
#
|
|
27
|
+
# In :ractor mode, `workers` is the number of worker Ractors: true
|
|
28
|
+
# multi-core parallelism for Ruby CPU work, one per core is a good start.
|
|
29
|
+
# In :threaded mode the same total (workers × threads) runs as plain
|
|
30
|
+
# Threads on the main ractor.
|
|
31
|
+
|
|
32
|
+
# Defaults to the number of CPU cores (Etc.nprocessors).
|
|
33
|
+
# workers 8
|
|
34
|
+
|
|
35
|
+
# Threads per worker. Threads inside one ractor share its lock, so they
|
|
36
|
+
# only add concurrency where handlers block on I/O (database calls, HTTP).
|
|
37
|
+
# CPU-bound apps gain nothing past 1 (and pay a lock-handoff tax: threads 1
|
|
38
|
+
# measured +17% on fast handlers). I/O-heavy apps want more SLOTS overall -
|
|
39
|
+
# in :ractor mode prefer raising `workers` over `threads` (slots are cheap,
|
|
40
|
+
# no fork memory): 32 workers x 1 thread beat 8x3 by +35% on waits.
|
|
41
|
+
# threads 3
|
|
42
|
+
|
|
43
|
+
## Dispatch mode
|
|
44
|
+
#
|
|
45
|
+
# :auto: :ractor when the app is Ractor-shareable, else :threaded
|
|
46
|
+
# (with a warning). Note: a Class used as a Rack app always
|
|
47
|
+
# counts as "shareable" even if calling it touches unshareable
|
|
48
|
+
# state; force :threaded for those.
|
|
49
|
+
# :ractor: require a Ractor-shareable app; raises
|
|
50
|
+
# Kino::UnshareableAppError otherwise. The app must capture
|
|
51
|
+
# nothing mutable: frozen middleware, Ractor.shareable_proc
|
|
52
|
+
# endpoints.
|
|
53
|
+
# :threaded: run ANY Rack app (Rails included) on a classic thread pool.
|
|
54
|
+
# mode :auto
|
|
55
|
+
|
|
56
|
+
## Backpressure
|
|
57
|
+
|
|
58
|
+
# Bounded request queue between the Rust front-end and Ruby workers.
|
|
59
|
+
# When it stays full past queue_timeout, clients get an immediate 503
|
|
60
|
+
# instead of waiting forever.
|
|
61
|
+
# queue_depth 1024
|
|
62
|
+
|
|
63
|
+
# Seconds a request may wait for queue space before the 503.
|
|
64
|
+
# queue_timeout 1.0
|
|
65
|
+
|
|
66
|
+
# Seconds the app gets to produce a response before the client receives a
|
|
67
|
+
# 504 instead. Off by default (nil = wait forever). The handler is NOT
|
|
68
|
+
# killed - its late response is dropped and its slot stays busy until it
|
|
69
|
+
# returns, so size this above your slowest legitimate endpoint.
|
|
70
|
+
# request_timeout 30
|
|
71
|
+
|
|
72
|
+
# Requests a worker may grab per queue visit. Values above 1 squeeze more
|
|
73
|
+
# throughput out of uniformly fast handlers, but add head-of-line blocking
|
|
74
|
+
# behind slow ones and stretch the effective queue depth - leave at 1
|
|
75
|
+
# unless your handlers are all sub-millisecond.
|
|
76
|
+
# batch 1
|
|
77
|
+
|
|
78
|
+
# EXPERIMENTAL lane dispatch: per-worker queues with awake-preferring
|
|
79
|
+
# assignment and work stealing. Cuts per-request wakeups for uniformly
|
|
80
|
+
# fast handlers; semantics under overload are slightly different (per-lane
|
|
81
|
+
# caps with brief dispatcher retries instead of one global queue).
|
|
82
|
+
# lanes false
|
|
83
|
+
|
|
84
|
+
# Native access log: one line per request to stdout, written by a
|
|
85
|
+
# Rust-side flusher thread - request threads never block on the log.
|
|
86
|
+
#
|
|
87
|
+
# On color terminals lines are tinted by status class (2xx green,
|
|
88
|
+
# 3xx yellow, 4xx maroon, 5xx bright red). This is the SERVER's view - it
|
|
89
|
+
# includes the 503 rejections your app never sees - and it interleaves
|
|
90
|
+
# cleanly with your app's own log (e.g. Rails') on stdout. See also
|
|
91
|
+
# Kino::Logger for routing the app log through the same async sink.
|
|
92
|
+
#
|
|
93
|
+
# Try enabling it in the development environment.
|
|
94
|
+
# log_requests false
|
|
95
|
+
|
|
96
|
+
## Lifecycle
|
|
97
|
+
|
|
98
|
+
# Graceful-shutdown drain deadline in seconds: in-flight requests get this
|
|
99
|
+
# long to finish; past it, their clients receive 500s and workers are
|
|
100
|
+
# reaped. A second INT/TERM force-exits immediately.
|
|
101
|
+
# shutdown_timeout 30
|
|
102
|
+
|
|
103
|
+
# Write the master PID here on start; removed on graceful shutdown.
|
|
104
|
+
# pidfile "tmp/pids/kino.pid"
|
|
105
|
+
|
|
106
|
+
## Runtime
|
|
107
|
+
|
|
108
|
+
# Threads for the tokio (Rust I/O) runtime. Default (nil) lets tokio use
|
|
109
|
+
# one per core: right for I/O-heavy apps. For CPU-heavy apps this is a
|
|
110
|
+
# real lever: `tokio_threads 1` + `threads 1` measured +26% on a pure-CPU
|
|
111
|
+
# benchmark (every spare thread is Ruby work you didn't run).
|
|
112
|
+
# tokio_threads 4
|
|
113
|
+
|
|
114
|
+
## App
|
|
115
|
+
|
|
116
|
+
# Rackup file the `kino` CLI loads (positional CLI argument wins).
|
|
117
|
+
# rackup "config.ru"
|
|
118
|
+
|
|
119
|
+
# Sets RACK_ENV (unless already set) before the app is loaded by the CLI.
|
|
120
|
+
# environment "production"
|
|
121
|
+
|
|
122
|
+
## Rails
|
|
123
|
+
#
|
|
124
|
+
# Rails runs on Kino TODAY in :threaded mode; uncomment for a Rails app:
|
|
125
|
+
#
|
|
126
|
+
# mode :threaded
|
|
127
|
+
# environment "production"
|
|
128
|
+
# threads 5 # match your database pool size
|
|
129
|
+
#
|
|
130
|
+
# Recommended Rails-side settings to pair with Kino:
|
|
131
|
+
# - config.eager_load = true and no code reloading (production defaults):
|
|
132
|
+
# Kino's workers serve concurrently; lazy class loading under
|
|
133
|
+
# concurrency is slow and, in ractor mode, unsafe.
|
|
134
|
+
# - Database pool >= workers × threads (config/database.yml `pool:`).
|
|
135
|
+
# - Rails.logger goes to stdout/stderr or a thread-safe device.
|
|
136
|
+
#
|
|
137
|
+
# Rails main is being ractorized, but
|
|
138
|
+
# Rails.application still captures unshareable state at boot; known
|
|
139
|
+
# blockers are documented in Kino's README. Track rails/rails main; when
|
|
140
|
+
# Ractor.make_shareable(Rails.application) succeeds, `mode :ractor` here
|
|
141
|
+
# is all you'll need to change.
|
data/lib/kino/version.rb
ADDED
data/lib/kino/worker.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kino
|
|
4
|
+
# @private
|
|
5
|
+
# The request loop. Identical for threaded and ractor modes.
|
|
6
|
+
#
|
|
7
|
+
# The default (batch 1) hot path allocates one Hash per request: the env
|
|
8
|
+
# arrives with the native request handle embedded under "kino.request",
|
|
9
|
+
# and the common complete-body response rides the fused
|
|
10
|
+
# respond_and_take_one call: ~one FFI crossing per request, no arrays.
|
|
11
|
+
#
|
|
12
|
+
# batch > 1 trades fairness for throughput: a worker grabs up to that
|
|
13
|
+
# many already-queued requests per crossing, adding head-of-line blocking
|
|
14
|
+
# behind slow handlers and stretching effective queue depth.
|
|
15
|
+
module Worker
|
|
16
|
+
RACK_INPUT = "rack.input"
|
|
17
|
+
KINO_REQUEST = "kino.request"
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def run(server_id, worker_id, app, batch_size = 1)
|
|
22
|
+
if batch_size <= 1
|
|
23
|
+
env = Native.take_one(server_id, worker_id)
|
|
24
|
+
env = handle_one(env, server_id, worker_id, app) while env
|
|
25
|
+
else
|
|
26
|
+
batch = Native.take_batch(server_id, worker_id, batch_size)
|
|
27
|
+
batch = process(batch, server_id, worker_id, app, batch_size) while batch
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# serve() returns this when the response did NOT ride a fused
|
|
32
|
+
# respond-and-take (streaming body or app error) and the caller must
|
|
33
|
+
# take the next request itself. Frozen: worker ractors read it.
|
|
34
|
+
NOT_FUSED = Object.new.freeze
|
|
35
|
+
|
|
36
|
+
# Handle one request; returns the next env (fused take) or nil.
|
|
37
|
+
def handle_one(env, server_id, worker_id, app)
|
|
38
|
+
result = serve(env, app) do |request, status, headers, chunks|
|
|
39
|
+
request.respond_and_take_one(server_id, worker_id, status, headers, chunks)
|
|
40
|
+
end
|
|
41
|
+
result.equal?(NOT_FUSED) ? Native.take_one(server_id, worker_id) : result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Handle every env in the batch; returns the next batch (the last
|
|
45
|
+
# simple response rides the fused respond_and_take) or nil on shutdown.
|
|
46
|
+
def process(batch, server_id, worker_id, app, batch_size)
|
|
47
|
+
last = batch.size - 1
|
|
48
|
+
batch.each_with_index do |env, index|
|
|
49
|
+
result = serve(env, app) do |request, status, headers, chunks|
|
|
50
|
+
if index == last
|
|
51
|
+
request.respond_and_take(server_id, worker_id, batch_size,
|
|
52
|
+
status, headers, chunks)
|
|
53
|
+
else
|
|
54
|
+
request.send_simple(status, headers, chunks)
|
|
55
|
+
NOT_FUSED
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
return result if index == last && !result.equal?(NOT_FUSED)
|
|
59
|
+
end
|
|
60
|
+
Native.take_batch(server_id, worker_id, batch_size)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Run one request through the app. Complete bodies are yielded so the
|
|
64
|
+
# caller picks plain vs fused delivery (the block's return value passes
|
|
65
|
+
# through after the body is closed); streaming bodies are delivered
|
|
66
|
+
# here and return NOT_FUSED. App errors must never kill the worker;
|
|
67
|
+
# hard crashes (Exception) are the supervisor's job; and `abort` does
|
|
68
|
+
# the right thing whether or not the response head already went out.
|
|
69
|
+
def serve(env, app)
|
|
70
|
+
request = env[KINO_REQUEST]
|
|
71
|
+
env[RACK_INPUT] ||= Input.new(request)
|
|
72
|
+
status, headers, body = app.call(env)
|
|
73
|
+
|
|
74
|
+
if body.respond_to?(:to_ary)
|
|
75
|
+
result = yield(request, status.to_i, headers, join_chunks(body.to_ary))
|
|
76
|
+
body.close if body.respond_to?(:close)
|
|
77
|
+
result
|
|
78
|
+
else
|
|
79
|
+
deliver_streaming(request, status.to_i, headers, body, env[RACK_INPUT])
|
|
80
|
+
NOT_FUSED
|
|
81
|
+
end
|
|
82
|
+
rescue => e
|
|
83
|
+
Native.log_error("#{e.class}: #{e.message}")
|
|
84
|
+
request.abort
|
|
85
|
+
NOT_FUSED
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def deliver_streaming(request, status, headers, body, input)
|
|
89
|
+
request.send_headers(status, headers)
|
|
90
|
+
if body.respond_to?(:call) && !body.respond_to?(:each)
|
|
91
|
+
# Rack 3 streaming body: the app drives a full-duplex stream whose
|
|
92
|
+
# read side is the request's existing rack.input (a fresh Input
|
|
93
|
+
# here would strand anything the app already buffered from it).
|
|
94
|
+
stream = Stream.new(request, input)
|
|
95
|
+
begin
|
|
96
|
+
body.call(stream)
|
|
97
|
+
ensure
|
|
98
|
+
stream.close
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
# Enumerable body: chunked transfer unless the app set content-length.
|
|
102
|
+
begin
|
|
103
|
+
body.each { |chunk| request.write_chunk(chunk) }
|
|
104
|
+
ensure
|
|
105
|
+
request.finish
|
|
106
|
+
body.close if body.respond_to?(:close)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def join_chunks(chunks)
|
|
112
|
+
# Single-chunk bodies (the common case) skip the join copy entirely:
|
|
113
|
+
# the native layer reads raw bytes, so encoding doesn't matter.
|
|
114
|
+
return chunks.first || "" if chunks.size <= 1
|
|
115
|
+
|
|
116
|
+
joined = (+"").force_encoding(Encoding::BINARY)
|
|
117
|
+
chunks.each { |chunk| joined << chunk.b }
|
|
118
|
+
joined
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private_class_method :handle_one, :process, :serve, :deliver_streaming,
|
|
122
|
+
:join_chunks
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/kino.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "kino/version"
|
|
4
|
+
require "kino/kino"
|
|
5
|
+
|
|
6
|
+
# A high-performance Ractor web server for Ruby 4.0+: Rack 3-based, with
|
|
7
|
+
# a Rust (tokio + hyper) front-end and Ractor-parallel Ruby workers, plus
|
|
8
|
+
# a threaded fallback mode for apps (such as Rails) that are not
|
|
9
|
+
# Ractor-shareable.
|
|
10
|
+
#
|
|
11
|
+
# The public API is {Kino::Server}, {Kino::Configuration}, {Kino::Check},
|
|
12
|
+
# {Kino::Logger}, and {Kino.sleep}; the `kino` executable is implemented
|
|
13
|
+
# by {Kino::CLI}.
|
|
14
|
+
module Kino
|
|
15
|
+
# Base class for all errors raised by Kino.
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
# Raised when mode: :ractor is forced but the app is not Ractor-shareable.
|
|
19
|
+
class UnshareableAppError < Error; end
|
|
20
|
+
|
|
21
|
+
# High-resolution sleep that bypasses the VM timer (whose wakeups inside
|
|
22
|
+
# non-main ractors are coarse: ~+2.5ms on Linux). Sleeps on the OS clock
|
|
23
|
+
# with the GVL released; chunked so Thread#kill and shutdown interrupts
|
|
24
|
+
# are honored between chunks.
|
|
25
|
+
#
|
|
26
|
+
# @param seconds [Numeric] how long to sleep; must be non-negative
|
|
27
|
+
# @return [nil]
|
|
28
|
+
# @raise [ArgumentError] for negative or non-numeric durations
|
|
29
|
+
def self.sleep(seconds)
|
|
30
|
+
remaining = Float(seconds)
|
|
31
|
+
raise ArgumentError, "sleep duration must be non-negative" if remaining.negative?
|
|
32
|
+
|
|
33
|
+
remaining = Native.sleep_chunk(remaining) while remaining.positive?
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
require_relative "kino/cli"
|
|
39
|
+
require_relative "kino/logger"
|
|
40
|
+
require_relative "kino/check"
|
|
41
|
+
require_relative "kino/input"
|
|
42
|
+
require_relative "kino/null_input"
|
|
43
|
+
require_relative "kino/errors_stream"
|
|
44
|
+
require_relative "kino/stream"
|
|
45
|
+
require_relative "kino/configuration"
|
|
46
|
+
require_relative "kino/worker"
|
|
47
|
+
require_relative "kino/ractor_supervisor"
|
|
48
|
+
require_relative "kino/server"
|
|
49
|
+
|
|
50
|
+
# Hand the frozen shareable singletons to the native layer: it sets them
|
|
51
|
+
# straight into each request's env, so the worker loop allocates neither
|
|
52
|
+
# an errors stream nor (for bodyless requests) an input object.
|
|
53
|
+
Kino::Native.register_defaults(Kino::ErrorsStream::INSTANCE, Kino::NullInput::INSTANCE)
|
data/sig/kino.rbs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Public interfaces only. Internals (Kino::Worker, Kino::RactorSupervisor,
|
|
2
|
+
# Kino::Input, Kino::NullInput, Kino::ErrorsStream, Kino::Stream,
|
|
3
|
+
# Kino::Native) are deliberately unsigned: they are implementation detail
|
|
4
|
+
# and may change without notice.
|
|
5
|
+
module Kino
|
|
6
|
+
VERSION: String
|
|
7
|
+
|
|
8
|
+
# Any Rack 3 application: responds to #call(env) with [status, headers, body].
|
|
9
|
+
type rack_app = untyped
|
|
10
|
+
|
|
11
|
+
type stats_hash = Hash[Symbol, untyped]
|
|
12
|
+
|
|
13
|
+
class Error < StandardError
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Raised when mode: :ractor is forced but the app is not Ractor-shareable.
|
|
17
|
+
class UnshareableAppError < Error
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# High-resolution sleep on the OS clock with the GVL released.
|
|
21
|
+
def self.sleep: (Numeric seconds) -> nil
|
|
22
|
+
|
|
23
|
+
class Server
|
|
24
|
+
attr_reader port: Integer?
|
|
25
|
+
attr_reader mode: Symbol
|
|
26
|
+
attr_reader bind: String
|
|
27
|
+
|
|
28
|
+
def tls?: () -> bool
|
|
29
|
+
|
|
30
|
+
# Settings precedence: explicit kwargs > config_file DSL > defaults.
|
|
31
|
+
def initialize: (rack_app app, ?config_file: String?, **untyped options) -> void
|
|
32
|
+
|
|
33
|
+
def start: () -> self
|
|
34
|
+
|
|
35
|
+
# Graceful shutdown: drain to the deadline, then abort stragglers.
|
|
36
|
+
def shutdown: (?timeout: Numeric?) -> nil
|
|
37
|
+
|
|
38
|
+
def wait: () -> untyped
|
|
39
|
+
|
|
40
|
+
# Live snapshot: config echo plus native counters (queued, in_flight,
|
|
41
|
+
# served, rejected, timeouts, respawns, lane_depths when lanes are on).
|
|
42
|
+
def stats: () -> stats_hash
|
|
43
|
+
|
|
44
|
+
# Production entry point: start, banner, signal traps, block until done.
|
|
45
|
+
def self.run: (rack_app app, **untyped opts) -> Server
|
|
46
|
+
|
|
47
|
+
# INT/TERM drain gracefully (second signal force-exits); USR1 prints stats.
|
|
48
|
+
def self.trap_signals: (Server server) -> void
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class Configuration
|
|
52
|
+
DEFAULTS: Hash[Symbol, untyped]
|
|
53
|
+
SETTINGS: Array[Symbol]
|
|
54
|
+
SAMPLE_TEMPLATE: String
|
|
55
|
+
|
|
56
|
+
# The fully commented sample config (see `kino --init`).
|
|
57
|
+
def self.sample: () -> String
|
|
58
|
+
|
|
59
|
+
# Write the sample config to path; raises Kino::Error if it exists
|
|
60
|
+
# unless force. Returns the path.
|
|
61
|
+
def self.write_sample: (String path, ?force: bool) -> String
|
|
62
|
+
|
|
63
|
+
def initialize: () -> void
|
|
64
|
+
|
|
65
|
+
def []: (Symbol key) -> untyped
|
|
66
|
+
|
|
67
|
+
# Raises ArgumentError for keys outside DEFAULTS.
|
|
68
|
+
def set: (Symbol key, untyped value) -> untyped
|
|
69
|
+
|
|
70
|
+
def set?: (Symbol key) -> bool
|
|
71
|
+
|
|
72
|
+
# Evaluate a Puma-style Ruby DSL config file into this configuration.
|
|
73
|
+
def load_file: (String path) -> self
|
|
74
|
+
|
|
75
|
+
def merge!: (Hash[Symbol, untyped] options) -> self
|
|
76
|
+
|
|
77
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
78
|
+
|
|
79
|
+
# to_h minus the CLI-only keys (:rackup, :environment); what Server.new
|
|
80
|
+
# accepts.
|
|
81
|
+
def server_options: () -> Hash[Symbol, untyped]
|
|
82
|
+
|
|
83
|
+
# The config-file DSL: one method per directive.
|
|
84
|
+
class DSL
|
|
85
|
+
def initialize: (Configuration config) -> void
|
|
86
|
+
|
|
87
|
+
def bind: (String host) -> untyped
|
|
88
|
+
def port: (int port) -> untyped
|
|
89
|
+
def workers: (int count) -> untyped
|
|
90
|
+
def threads: (int count) -> untyped
|
|
91
|
+
def mode: (Symbol | String mode) -> untyped
|
|
92
|
+
def queue_depth: (int depth) -> untyped
|
|
93
|
+
def queue_timeout: (Numeric seconds) -> untyped
|
|
94
|
+
def request_timeout: (Numeric? seconds) -> untyped
|
|
95
|
+
def batch: (int count) -> untyped
|
|
96
|
+
def lanes: (boolish enabled) -> untyped
|
|
97
|
+
def log_requests: (boolish enabled) -> untyped
|
|
98
|
+
def shutdown_timeout: (Numeric seconds) -> untyped
|
|
99
|
+
def tokio_threads: (int count) -> untyped
|
|
100
|
+
def tls: (cert: String, key: String) -> untyped
|
|
101
|
+
def environment: (String | Symbol env) -> untyped
|
|
102
|
+
def pidfile: (String path) -> untyped
|
|
103
|
+
def rackup: (String path) -> untyped
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The `kino` executable plus startup presentation shared with Server.run.
|
|
108
|
+
module CLI
|
|
109
|
+
MOTD: String
|
|
110
|
+
|
|
111
|
+
# Parse argv, then init/check/serve. Returns the process exit status
|
|
112
|
+
# (-v and -h print and exit in place per optparse convention).
|
|
113
|
+
def self.start: (Array[String] argv) -> Integer
|
|
114
|
+
|
|
115
|
+
# True when output to io may use ANSI styling.
|
|
116
|
+
def self.color?: (?IO io) -> bool
|
|
117
|
+
|
|
118
|
+
# Wrap text in an SGR code, resetting at the end; plain on non-color io.
|
|
119
|
+
def self.paint: (String code, String text, ?io: IO) -> String
|
|
120
|
+
|
|
121
|
+
def self.dim: (String text, ?io: IO) -> String
|
|
122
|
+
|
|
123
|
+
def self.bold: (String text, ?io: IO) -> String
|
|
124
|
+
|
|
125
|
+
def self.red: (String text, ?io: IO) -> String
|
|
126
|
+
|
|
127
|
+
# The banner with a vertical grayscale gradient.
|
|
128
|
+
def self.motd: (?color: bool) -> String
|
|
129
|
+
|
|
130
|
+
# One-line stats dump (the SIGUSR1 handler's output).
|
|
131
|
+
def self.stats_line: (stats_hash stats) -> String
|
|
132
|
+
|
|
133
|
+
# The two banner halves around Server#start.
|
|
134
|
+
def self.opening_credits: () -> void
|
|
135
|
+
def self.action!: (Server server) -> void
|
|
136
|
+
|
|
137
|
+
# Print a bold "Fin." when the process ends (idempotent).
|
|
138
|
+
def self.fin_at_exit: () -> void
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The shareability doctor behind `kino --check`.
|
|
142
|
+
module Check
|
|
143
|
+
MAX_FINDINGS: Integer
|
|
144
|
+
MAX_NODES: Integer
|
|
145
|
+
|
|
146
|
+
class Finding < ::Struct[untyped]
|
|
147
|
+
attr_accessor path: String
|
|
148
|
+
attr_accessor message: String
|
|
149
|
+
|
|
150
|
+
def to_s: () -> String
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.report: (rack_app app) -> { shareable: bool, findings: Array[Finding] }
|
|
154
|
+
|
|
155
|
+
# Pretty-printed report; true when the app is ractor-ready.
|
|
156
|
+
def self.print_report: (rack_app app, ?io: IO) -> bool
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# A ::Logger writing through the native async sink.
|
|
160
|
+
class Logger < ::Logger
|
|
161
|
+
# path: a file (created/appended) or nil for stdout.
|
|
162
|
+
def initialize: (?String? path, **untyped options) -> void
|
|
163
|
+
|
|
164
|
+
# The raw IO-like device: frozen and Ractor-shareable, one device can
|
|
165
|
+
# serve every worker.
|
|
166
|
+
class Device
|
|
167
|
+
def initialize: (?String? path) -> void
|
|
168
|
+
|
|
169
|
+
def write: (untyped message) -> void
|
|
170
|
+
|
|
171
|
+
alias << write
|
|
172
|
+
|
|
173
|
+
def close: () -> void
|
|
174
|
+
|
|
175
|
+
def reopen: (*untyped) -> self
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kino
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: aarch64-linux
|
|
6
|
+
authors:
|
|
7
|
+
- Yaroslav Markin
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: logger
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.6'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.6'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rack
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake-compiler
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rbs
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '4.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '4.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rspec
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: standard
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '1.50'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '1.50'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: yard
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0.9'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0.9'
|
|
125
|
+
description: 'A high-performance Ractor web server for Ruby 4.0+: Rack 3-based, with
|
|
126
|
+
a Rust tokio/hyper front-end and Ractor-parallel Ruby workers and threaded fallback
|
|
127
|
+
mode.'
|
|
128
|
+
email:
|
|
129
|
+
- yaroslav@markin.net
|
|
130
|
+
executables:
|
|
131
|
+
- kino
|
|
132
|
+
extensions: []
|
|
133
|
+
extra_rdoc_files: []
|
|
134
|
+
files:
|
|
135
|
+
- ".yardopts"
|
|
136
|
+
- CHANGELOG.md
|
|
137
|
+
- LICENSE.txt
|
|
138
|
+
- README.md
|
|
139
|
+
- doc/README.md
|
|
140
|
+
- doc/architecture.md
|
|
141
|
+
- doc/benchmarks.md
|
|
142
|
+
- doc/rails-on-ractors.md
|
|
143
|
+
- doc/why-kino.md
|
|
144
|
+
- exe/kino
|
|
145
|
+
- lib/kino.rb
|
|
146
|
+
- lib/kino/check.rb
|
|
147
|
+
- lib/kino/cli.rb
|
|
148
|
+
- lib/kino/configuration.rb
|
|
149
|
+
- lib/kino/errors_stream.rb
|
|
150
|
+
- lib/kino/input.rb
|
|
151
|
+
- lib/kino/kino.so
|
|
152
|
+
- lib/kino/logger.rb
|
|
153
|
+
- lib/kino/null_input.rb
|
|
154
|
+
- lib/kino/ractor_supervisor.rb
|
|
155
|
+
- lib/kino/server.rb
|
|
156
|
+
- lib/kino/stream.rb
|
|
157
|
+
- lib/kino/templates/kino.rb.tt
|
|
158
|
+
- lib/kino/version.rb
|
|
159
|
+
- lib/kino/worker.rb
|
|
160
|
+
- sig/kino.rbs
|
|
161
|
+
homepage: https://github.com/yaroslav/kino
|
|
162
|
+
licenses:
|
|
163
|
+
- MIT
|
|
164
|
+
metadata:
|
|
165
|
+
allowed_push_host: https://rubygems.org
|
|
166
|
+
homepage_uri: https://github.com/yaroslav/kino
|
|
167
|
+
source_code_uri: https://github.com/yaroslav/kino
|
|
168
|
+
changelog_uri: https://github.com/yaroslav/kino/blob/main/CHANGELOG.md
|
|
169
|
+
documentation_uri: https://rubydoc.info/gems/kino
|
|
170
|
+
rubygems_mfa_required: 'true'
|
|
171
|
+
post_install_message:
|
|
172
|
+
rdoc_options: []
|
|
173
|
+
require_paths:
|
|
174
|
+
- lib
|
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '4.0'
|
|
180
|
+
- - "<"
|
|
181
|
+
- !ruby/object:Gem::Version
|
|
182
|
+
version: 4.1.dev
|
|
183
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
184
|
+
requirements:
|
|
185
|
+
- - ">="
|
|
186
|
+
- !ruby/object:Gem::Version
|
|
187
|
+
version: '0'
|
|
188
|
+
requirements: []
|
|
189
|
+
rubygems_version: 3.5.23
|
|
190
|
+
signing_key:
|
|
191
|
+
specification_version: 4
|
|
192
|
+
summary: High-performance Ractor web server for Ruby.
|
|
193
|
+
test_files: []
|