kino 0.4.0 → 0.6.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/ext/kino/src/tls.rs CHANGED
@@ -17,7 +17,13 @@ fn pem_bytes(input: &str) -> Result<Vec<u8>, String> {
17
17
  }
18
18
  }
19
19
 
20
- pub fn build_acceptor(cert: &str, key: &str) -> Result<TlsAcceptor, String> {
20
+ pub fn build_acceptor(cert: &str, key: &str, http2: bool) -> Result<TlsAcceptor, String> {
21
+ Ok(TlsAcceptor::from(Arc::new(server_config(
22
+ cert, key, http2,
23
+ )?)))
24
+ }
25
+
26
+ fn server_config(cert: &str, key: &str, http2: bool) -> Result<ServerConfig, String> {
21
27
  let cert_pem = pem_bytes(cert)?;
22
28
  let key_pem = pem_bytes(key)?;
23
29
 
@@ -36,14 +42,19 @@ pub fn build_acceptor(cert: &str, key: &str) -> Result<TlsAcceptor, String> {
36
42
  .with_no_client_auth()
37
43
  .with_single_cert(certs, key)
38
44
  .map_err(|e| format!("TLS config rejected: {e}"))?;
39
- config.alpn_protocols = vec![b"http/1.1".to_vec()];
40
-
41
- Ok(TlsAcceptor::from(Arc::new(config)))
45
+ // Preference order matters: h2 first, so an HTTP/2-capable client
46
+ // negotiates it; a client offering only http/1.1 still matches.
47
+ config.alpn_protocols = if http2 {
48
+ vec![b"h2".to_vec(), b"http/1.1".to_vec()]
49
+ } else {
50
+ vec![b"http/1.1".to_vec()]
51
+ };
52
+ Ok(config)
42
53
  }
43
54
 
44
55
  #[cfg(test)]
45
56
  mod tests {
46
- use super::build_acceptor;
57
+ use super::{build_acceptor, server_config};
47
58
 
48
59
  const CERT: &str = "-----BEGIN CERTIFICATE-----
49
60
  MIIBfjCCASWgAwIBAgIUTs9+cVJSzJjy4TSi9YLEd+i4KiIwCgYIKoZIzj0EAwIw
@@ -65,32 +76,138 @@ WJ2lRijROyX9v7f8aSQlb6kEwKhI8kG8SbeUc+zbKkzGgRXNaZHY/mAa
65
76
 
66
77
  #[test]
67
78
  fn inline_pem_builds_an_acceptor() {
68
- assert!(build_acceptor(CERT, KEY).is_ok());
79
+ assert!(build_acceptor(CERT, KEY, true).is_ok());
69
80
  }
70
81
 
71
82
  #[test]
72
83
  fn missing_file_paths_error_without_panicking() {
73
- let err = build_acceptor("/nonexistent/cert.pem", "/nonexistent/key.pem")
74
- .err().expect("missing files");
84
+ let err = build_acceptor("/nonexistent/cert.pem", "/nonexistent/key.pem", true)
85
+ .err()
86
+ .expect("missing files");
75
87
  assert!(err.contains("cannot read"));
76
88
  }
77
89
 
78
90
  #[test]
79
91
  fn pem_without_certificates_is_rejected() {
80
- let err = build_acceptor(KEY, KEY).err().expect("a key is not a cert");
92
+ let err = build_acceptor(KEY, KEY, true)
93
+ .err()
94
+ .expect("a key is not a cert");
81
95
  assert!(err.contains("no certificates found"));
82
96
  }
83
97
 
84
98
  #[test]
85
99
  fn pem_without_a_key_is_rejected() {
86
- let err = build_acceptor(CERT, CERT).err().expect("a cert is not a key");
100
+ let err = build_acceptor(CERT, CERT, true)
101
+ .err()
102
+ .expect("a cert is not a key");
87
103
  assert!(err.contains("no private key found"));
88
104
  }
89
105
 
90
106
  #[test]
91
107
  fn mismatched_cert_and_garbage_key_are_rejected() {
92
- let err = build_acceptor(CERT, "-----BEGIN PRIVATE KEY-----\ngarbage\n-----END PRIVATE KEY-----")
93
- .err().expect("garbage key");
108
+ let err = build_acceptor(
109
+ CERT,
110
+ "-----BEGIN PRIVATE KEY-----\ngarbage\n-----END PRIVATE KEY-----",
111
+ true,
112
+ )
113
+ .err()
114
+ .expect("garbage key");
94
115
  assert!(!err.is_empty());
95
116
  }
117
+
118
+ #[test]
119
+ fn alpn_advertises_h2_first_when_http2_is_on() {
120
+ let config = server_config(CERT, KEY, true).expect("valid PEM");
121
+ assert_eq!(
122
+ config.alpn_protocols,
123
+ vec![b"h2".to_vec(), b"http/1.1".to_vec()]
124
+ );
125
+ }
126
+
127
+ #[test]
128
+ fn alpn_advertises_only_http11_when_http2_is_off() {
129
+ let config = server_config(CERT, KEY, false).expect("valid PEM");
130
+ assert_eq!(config.alpn_protocols, vec![b"http/1.1".to_vec()]);
131
+ }
132
+
133
+ /// A real rustls handshake over an in-memory pipe: an h2-capable
134
+ /// client must land on "h2", proving the advertisement is not just a
135
+ /// config field but what the wire negotiates. The client skips
136
+ /// certificate verification (the fixture cert is CA-flagged, which
137
+ /// webpki refuses as an end-entity cert): ALPN is under test here,
138
+ /// not the trust chain.
139
+ #[tokio::test]
140
+ async fn handshake_negotiates_h2_with_a_capable_client() {
141
+ use std::sync::Arc;
142
+
143
+ use tokio_rustls::rustls::client::danger::{
144
+ HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
145
+ };
146
+ use tokio_rustls::rustls::crypto::WebPkiSupportedAlgorithms;
147
+ use tokio_rustls::rustls::pki_types::ServerName;
148
+ use tokio_rustls::rustls::ClientConfig;
149
+ use tokio_rustls::{TlsAcceptor, TlsConnector};
150
+
151
+ #[derive(Debug)]
152
+ struct AcceptAny(WebPkiSupportedAlgorithms);
153
+
154
+ impl ServerCertVerifier for AcceptAny {
155
+ fn verify_server_cert(
156
+ &self,
157
+ _end_entity: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
158
+ _intermediates: &[tokio_rustls::rustls::pki_types::CertificateDer<'_>],
159
+ _server_name: &ServerName<'_>,
160
+ _ocsp_response: &[u8],
161
+ _now: tokio_rustls::rustls::pki_types::UnixTime,
162
+ ) -> Result<ServerCertVerified, tokio_rustls::rustls::Error> {
163
+ Ok(ServerCertVerified::assertion())
164
+ }
165
+
166
+ fn verify_tls12_signature(
167
+ &self,
168
+ _message: &[u8],
169
+ _cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
170
+ _dss: &tokio_rustls::rustls::DigitallySignedStruct,
171
+ ) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
172
+ Ok(HandshakeSignatureValid::assertion())
173
+ }
174
+
175
+ fn verify_tls13_signature(
176
+ &self,
177
+ _message: &[u8],
178
+ _cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
179
+ _dss: &tokio_rustls::rustls::DigitallySignedStruct,
180
+ ) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
181
+ Ok(HandshakeSignatureValid::assertion())
182
+ }
183
+
184
+ fn supported_verify_schemes(&self) -> Vec<tokio_rustls::rustls::SignatureScheme> {
185
+ self.0.supported_schemes()
186
+ }
187
+ }
188
+
189
+ let acceptor =
190
+ TlsAcceptor::from(Arc::new(server_config(CERT, KEY, true).expect("valid PEM")));
191
+
192
+ let provider = tokio_rustls::rustls::crypto::ring::default_provider();
193
+ let verifier = AcceptAny(provider.signature_verification_algorithms);
194
+ let mut client = ClientConfig::builder()
195
+ .dangerous()
196
+ .with_custom_certificate_verifier(Arc::new(verifier))
197
+ .with_no_client_auth();
198
+ client.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
199
+ let connector = TlsConnector::from(Arc::new(client));
200
+
201
+ let (client_io, server_io) = tokio::io::duplex(16 * 1024);
202
+ let server = tokio::spawn(async move { acceptor.accept(server_io).await });
203
+ let domain = ServerName::try_from("localhost").expect("server name");
204
+ let client_stream = connector
205
+ .connect(domain, client_io)
206
+ .await
207
+ .expect("client handshake");
208
+ let server_stream = server.await.expect("join").expect("server handshake");
209
+
210
+ assert_eq!(client_stream.get_ref().1.alpn_protocol(), Some(&b"h2"[..]));
211
+ assert_eq!(server_stream.get_ref().1.alpn_protocol(), Some(&b"h2"[..]));
212
+ }
96
213
  }
@@ -26,8 +26,11 @@ module Kino
26
26
  after_request_complete: nil,
27
27
  on_worker_exit: nil,
28
28
  shutdown_timeout: 30,
29
+ io_shards: false,
30
+ io_threads: nil,
29
31
  tokio_threads: nil,
30
32
  tls: nil,
33
+ http2: true,
31
34
  environment: nil,
32
35
  pidfile: nil,
33
36
  control_bind: nil,
@@ -148,6 +151,8 @@ module Kino
148
151
  # queue_depth 2048
149
152
  # queue_timeout 0.5
150
153
  # shutdown_timeout 15
154
+ # io_shards true
155
+ # io_threads 6
151
156
  # tokio_threads 4
152
157
  # tls cert: "cert.pem", key: "key.pem"
153
158
  #
@@ -229,12 +234,23 @@ module Kino
229
234
  # Graceful-shutdown drain deadline in seconds.
230
235
  def shutdown_timeout(seconds) = @config.set(:shutdown_timeout, seconds)
231
236
 
237
+ # Run native HTTP I/O on current-thread shards instead of Tokio's shared pool.
238
+ def io_shards(enabled = true) = @config.set(:io_shards, !!enabled)
239
+
240
+ # Native HTTP I/O shard count; default with io_shards: half available CPUs.
241
+ def io_threads(count) = @config.set(:io_threads, Integer(count))
242
+
232
243
  # Threads for the tokio (Rust I/O) runtime; default: one per core.
233
244
  def tokio_threads(count) = @config.set(:tokio_threads, Integer(count))
234
245
 
235
246
  # TLS termination; file paths or inline PEM strings.
236
247
  def tls(cert:, key:) = @config.set(:tls, {cert: cert, key: key})
237
248
 
249
+ # HTTP/2: negotiated via ALPN on TLS binds, served to
250
+ # prior-knowledge clients on plaintext. On by default; set false
251
+ # to serve HTTP/1 only.
252
+ def http2(enabled = true) = @config.set(:http2, !!enabled)
253
+
238
254
  # Sets RACK_ENV (unless already set) before the CLI loads the app.
239
255
  def environment(env) = @config.set(:environment, env.to_s)
240
256
 
data/lib/kino/server.rb CHANGED
@@ -98,11 +98,18 @@ module Kino
98
98
  @lanes = !!settings[:lanes]
99
99
  @log_requests = !!settings[:log_requests]
100
100
  @shutdown_timeout = settings[:shutdown_timeout]
101
+ @io_shards = !!settings[:io_shards]
102
+ @io_threads = Integer(settings[:io_threads]) unless settings[:io_threads].nil?
103
+ if @io_threads && @io_threads < 1
104
+ raise ArgumentError, "io_threads must be >= 1"
105
+ end
106
+ Log.warn("io_threads has no effect unless io_shards is true") if @io_threads && !@io_shards
101
107
  @tokio_threads = settings[:tokio_threads]
102
108
  @tls = validate_tls(settings[:tls])
103
109
  if @tls && unix?
104
110
  raise ArgumentError, "TLS is not supported on a unix socket bind; terminate TLS at the proxy in front"
105
111
  end
112
+ @http2 = settings.fetch(:http2, true) ? true : false
106
113
  @pidfile = settings[:pidfile]
107
114
  @control_bind = settings[:control_bind]&.to_s
108
115
  @control_token = settings[:control_token]&.to_s
@@ -145,8 +152,11 @@ module Kino
145
152
  request_timeout_ms: @request_timeout_ms,
146
153
  max_connections: @max_connections,
147
154
  max_body_size: @max_body_size,
155
+ io_shards: @io_shards,
156
+ io_threads: @io_threads,
148
157
  tokio_threads: @tokio_threads,
149
158
  tls_cert: @tls&.fetch(:cert), tls_key: @tls&.fetch(:key),
159
+ http2: @http2,
150
160
  lanes: @lanes, log_requests: @log_requests,
151
161
  mode: @mode.to_s, workers: @workers, threads: @threads, batch: @batch,
152
162
  control_bind: @control_bind, control_token: @control_token
@@ -20,6 +20,11 @@
20
20
  # PEM strings also work).
21
21
  # tls cert: "config/certs/server.pem", key: "config/certs/server.key"
22
22
 
23
+ # HTTP/2 is on by default: negotiated via ALPN on TLS binds, and served
24
+ # to prior-knowledge (h2c) clients on plaintext. Everything else keeps
25
+ # getting HTTP/1.1. Set false to serve HTTP/1 only.
26
+ # http2 false
27
+
23
28
  ## Topology
24
29
 
25
30
  # How many workers to run. Each worker handles requests independently;
@@ -125,8 +130,14 @@
125
130
 
126
131
  ## Runtime
127
132
 
128
- # Threads for the Rust I/O engine. The default suits most apps; for
129
- # heavily CPU-bound apps, try 1 to leave more cores for Ruby.
133
+ # Run native HTTP I/O on current-thread shards instead of Tokio's shared
134
+ # worker pool, reducing scheduler contention on very fast handlers.
135
+ # io_shards true
136
+
137
+ # I/O shard count. Default with io_shards: half available CPUs.
138
+ # io_threads 6
139
+
140
+ # Threads for the Tokio multi-thread runtime. Default: one per available CPU.
130
141
  # tokio_threads 4
131
142
 
132
143
  ## Control plane
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.4.0"
5
+ VERSION = "0.6.0"
6
6
  end
data/lib/kino/worker.rb CHANGED
@@ -20,12 +20,16 @@ module Kino
20
20
 
21
21
  def run(server_id, worker_id, app, batch_size = 1, hooks = nil)
22
22
  fire_after_worker_boot(hooks, worker_id)
23
+ # One resolution of (server, worker) into a native handle; every take
24
+ # after this skips the registry lookup. nil = server already gone.
25
+ worker = Native.worker(server_id, worker_id)
26
+ return unless worker
23
27
  if batch_size <= 1
24
- env = Native.take_one(server_id, worker_id)
25
- env = handle_one(env, server_id, worker_id, app, hooks) while env
28
+ env = worker.take_one
29
+ env = handle_one(env, worker, app, hooks) while env
26
30
  else
27
- batch = Native.take_batch(server_id, worker_id, batch_size)
28
- batch = process(batch, server_id, worker_id, app, batch_size, hooks) while batch
31
+ batch = worker.take_batch(batch_size)
32
+ batch = process(batch, worker, app, batch_size, hooks) while batch
29
33
  end
30
34
  end
31
35
 
@@ -35,22 +39,21 @@ module Kino
35
39
  NOT_FUSED = Object.new.freeze
36
40
 
37
41
  # Handle one request; returns the next env (fused take) or nil.
38
- def handle_one(env, server_id, worker_id, app, hooks)
42
+ def handle_one(env, worker, app, hooks)
39
43
  result = serve(env, app, hooks) do |request, status, headers, chunks|
40
- request.respond_and_take_one(server_id, worker_id, status, headers, chunks)
44
+ request.respond_and_take_one(worker, status, headers, chunks)
41
45
  end
42
- result.equal?(NOT_FUSED) ? Native.take_one(server_id, worker_id) : result
46
+ result.equal?(NOT_FUSED) ? worker.take_one : result
43
47
  end
44
48
 
45
49
  # Handle every env in the batch; returns the next batch (the last
46
50
  # simple response rides the fused respond_and_take) or nil on shutdown.
47
- def process(batch, server_id, worker_id, app, batch_size, hooks)
51
+ def process(batch, worker, app, batch_size, hooks)
48
52
  last = batch.size - 1
49
53
  batch.each_with_index do |env, index|
50
54
  result = serve(env, app, hooks) do |request, status, headers, chunks|
51
55
  if index == last
52
- request.respond_and_take(server_id, worker_id, batch_size,
53
- status, headers, chunks)
56
+ request.respond_and_take(worker, batch_size, status, headers, chunks)
54
57
  else
55
58
  request.send_simple(status, headers, chunks)
56
59
  NOT_FUSED
@@ -58,7 +61,7 @@ module Kino
58
61
  end
59
62
  return result if index == last && !result.equal?(NOT_FUSED)
60
63
  end
61
- Native.take_batch(server_id, worker_id, batch_size)
64
+ worker.take_batch(batch_size)
62
65
  end
63
66
 
64
67
  # Run one request through the app. Complete bodies are yielded so the
data/sig/kino.rbs CHANGED
@@ -126,8 +126,11 @@ module Kino
126
126
  def after_request_complete: (?^(Hash[String, untyped], Integer) -> void handler) ?{ (Hash[String, untyped], Integer) -> void } -> untyped
127
127
  def on_worker_exit: (?^(Integer, Exception?) -> void handler) ?{ (Integer, Exception?) -> void } -> untyped
128
128
  def shutdown_timeout: (Numeric seconds) -> untyped
129
+ def io_shards: (?boolish enabled) -> untyped
130
+ def io_threads: (int? count) -> untyped
129
131
  def tokio_threads: (int count) -> untyped
130
132
  def tls: (cert: String, key: String) -> untyped
133
+ def http2: (?boolish enabled) -> untyped
131
134
  def environment: (String | Symbol env) -> untyped
132
135
  def pidfile: (String path) -> untyped
133
136
  def control_bind: (String addr) -> String
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kino
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Markin
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: 0.9.128
54
+ - !ruby/object:Gem::Dependency
55
+ name: httpx
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.3'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.3'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: rackup
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -180,6 +194,7 @@ files:
180
194
  - ext/kino/src/cpus.rs
181
195
  - ext/kino/src/env_strings.rs
182
196
  - ext/kino/src/gvl.rs
197
+ - ext/kino/src/io_shards.rs
183
198
  - ext/kino/src/lib.rs
184
199
  - ext/kino/src/listen.rs
185
200
  - ext/kino/src/log.rs