kino 0.5.0 → 0.7.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 +67 -0
- data/Cargo.lock +92 -1
- data/README.md +153 -44
- data/doc/architecture.md +64 -3
- data/doc/benchmarks.md +120 -26
- data/ext/kino/Cargo.toml +8 -3
- data/ext/kino/src/control.rs +103 -6
- data/ext/kino/src/env_strings.rs +350 -69
- data/ext/kino/src/lib.rs +22 -7
- data/ext/kino/src/pin.rs +21 -9
- data/ext/kino/src/queue.rs +158 -46
- data/ext/kino/src/registry.rs +145 -0
- data/ext/kino/src/request.rs +151 -70
- data/ext/kino/src/response.rs +39 -5
- data/ext/kino/src/server.rs +864 -33
- data/ext/kino/src/test_support.rs +44 -0
- data/ext/kino/src/tls.rs +119 -9
- data/lib/kino/cli.rb +12 -4
- data/lib/kino/configuration.rb +22 -2
- data/lib/kino/monitor.rb +52 -0
- data/lib/kino/pool_scaler.rb +103 -0
- data/lib/kino/quarantine_monitor.rb +5 -37
- data/lib/kino/ractor_supervisor.rb +102 -8
- data/lib/kino/server.rb +66 -85
- data/lib/kino/slot_bank.rb +31 -0
- data/lib/kino/templates/kino.rb.tt +20 -0
- data/lib/kino/threaded_pool.rb +186 -0
- data/lib/kino/version.rb +1 -1
- data/lib/kino/worker.rb +14 -11
- data/lib/kino.rb +4 -0
- data/lib/rackup/handler/kino.rb +4 -4
- data/sig/kino.rbs +3 -0
- metadata +19 -1
data/ext/kino/src/env_strings.rs
CHANGED
|
@@ -8,19 +8,40 @@
|
|
|
8
8
|
//! request headers.
|
|
9
9
|
//!
|
|
10
10
|
//! `Opaque<RString>` is magnus's sanctioned way to keep Ruby values in
|
|
11
|
-
//! statics (it is Send + Sync); GC registration makes
|
|
12
|
-
//! once, with the GVL held, on the main ractor; read-only
|
|
11
|
+
//! statics (it is Send + Sync); GC registration makes the static ones
|
|
12
|
+
//! immortal. Built once, with the GVL held, on the main ractor; read-only
|
|
13
|
+
//! afterwards.
|
|
14
|
+
//!
|
|
15
|
+
//! The LRU caches (hosts, peer addresses, interned header values) are the
|
|
16
|
+
//! exception: they insert and evict from every worker ractor in parallel.
|
|
17
|
+
//! That rules out per-value GC registration, because in Ruby 4.0
|
|
18
|
+
//! `rb_gc_register_address` and its unregister walk a VM-wide linked list
|
|
19
|
+
//! with no lock. Their strings are rooted through a [`PinSlab`] instead:
|
|
20
|
+
//! one atomic VALUE slot per cached string, marked from a keeper object
|
|
21
|
+
//! registered once at init. The same parallelism forbids any Ruby call
|
|
22
|
+
//! while a cache lock is held: a Ruby allocation can start a GC, a GC
|
|
23
|
+
//! with several ractors stops at a barrier every other ractor must
|
|
24
|
+
//! reach, and a ractor parked on the cache mutex never would. So the
|
|
25
|
+
//! caches allocate their strings first, then lock only to look up, root,
|
|
26
|
+
//! insert, or evict.
|
|
13
27
|
|
|
28
|
+
use std::borrow::Borrow;
|
|
29
|
+
use std::hash::Hash;
|
|
14
30
|
use std::net::IpAddr;
|
|
15
|
-
use std::sync::OnceLock;
|
|
31
|
+
use std::sync::{Arc, OnceLock};
|
|
16
32
|
|
|
33
|
+
use magnus::rb_sys::AsRawValue;
|
|
17
34
|
use magnus::value::Opaque;
|
|
18
35
|
use magnus::{gc, prelude::*, RString, Ruby, Value};
|
|
19
36
|
use parking_lot::{Mutex, RwLock};
|
|
20
37
|
|
|
38
|
+
use crate::pin::{PinKeeper, PinSlab};
|
|
39
|
+
|
|
21
40
|
/// These maps are probed several times per request; ahash beats the
|
|
22
41
|
/// DoS-resistant default since every key here is our own static data.
|
|
23
42
|
type HashMap<K, V> = std::collections::HashMap<K, V, ahash::RandomState>;
|
|
43
|
+
type HashSet<K> = std::collections::HashSet<K, ahash::RandomState>;
|
|
44
|
+
type LruCache<K, V> = lru::LruCache<K, V, ahash::RandomState>;
|
|
24
45
|
|
|
25
46
|
pub struct EnvStrings {
|
|
26
47
|
// keys
|
|
@@ -44,17 +65,28 @@ pub struct EnvStrings {
|
|
|
44
65
|
pub https: Opaque<RString>,
|
|
45
66
|
pub http10: Opaque<RString>,
|
|
46
67
|
pub http11: Opaque<RString>,
|
|
68
|
+
pub http2: Opaque<RString>,
|
|
47
69
|
pub methods: HashMap<&'static str, Opaque<RString>>,
|
|
48
70
|
/// lowercase header name -> frozen "HTTP_<UPPER>" key
|
|
49
71
|
pub header_names: HashMap<&'static str, Opaque<RString>>,
|
|
50
|
-
///
|
|
51
|
-
///
|
|
52
|
-
///
|
|
53
|
-
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
/// GC roots for every string the LRU caches below hold (see the
|
|
73
|
+
/// module docs); a cache entry owns its slots and clears them when
|
|
74
|
+
/// it is evicted.
|
|
75
|
+
pub slab: Arc<PinSlab>,
|
|
76
|
+
/// Host-header or :authority bytes -> frozen host values, and peer
|
|
77
|
+
/// IP -> frozen REMOTE_ADDR value. Real traffic has low cardinality
|
|
78
|
+
/// on both, so these kill 3 string allocations per request.
|
|
79
|
+
/// LRU-bounded, so a rotating-host attack recycles cache slots
|
|
80
|
+
/// instead of accumulating immortal strings.
|
|
81
|
+
pub hosts: Mutex<LruCache<Vec<u8>, HostEntry>>,
|
|
82
|
+
pub addrs: Mutex<LruCache<IpAddr, CachedStr>>,
|
|
83
|
+
/// Interned values of low-cardinality headers (see
|
|
84
|
+
/// [`INTERNABLE_VALUES`]): value bytes -> frozen RString, shared
|
|
85
|
+
/// across headers that happen to carry the same bytes. Same rooting
|
|
86
|
+
/// and locking contract as `hosts`/`addrs`.
|
|
87
|
+
pub values: Mutex<LruCache<Vec<u8>, CachedStr>>,
|
|
88
|
+
/// The names whose values go through the `values` cache.
|
|
89
|
+
pub internable: HashSet<&'static str>,
|
|
58
90
|
/// Ractor-shareable defaults provided by the Ruby layer at boot:
|
|
59
91
|
/// the frozen rack.errors writer and the frozen null rack.input.
|
|
60
92
|
pub errors_stream: RwLock<Option<Opaque<Value>>>,
|
|
@@ -63,29 +95,131 @@ pub struct EnvStrings {
|
|
|
63
95
|
|
|
64
96
|
const HOST_CACHE_CAP: usize = 256;
|
|
65
97
|
const ADDR_CACHE_CAP: usize = 1024;
|
|
98
|
+
const VALUE_CACHE_CAP: usize = 512;
|
|
99
|
+
|
|
100
|
+
/// Slab slots for every string the three caches can hold at once: up to
|
|
101
|
+
/// three per host entry, one per address and per interned value, plus
|
|
102
|
+
/// one entry per cache for the insert that precedes an eviction. A full
|
|
103
|
+
/// slab (which this bound rules out) degrades to an uncached string,
|
|
104
|
+
/// never to an unrooted one.
|
|
105
|
+
const CACHE_SLAB_CAPACITY: usize =
|
|
106
|
+
(HOST_CACHE_CAP + 1) * 3 + (ADDR_CACHE_CAP + 1) + (VALUE_CACHE_CAP + 1);
|
|
66
107
|
|
|
67
|
-
///
|
|
68
|
-
///
|
|
108
|
+
/// Values longer than this are never interned: past it the memcpy into
|
|
109
|
+
/// a fresh Ruby string is cheap relative to the bytes themselves, and
|
|
110
|
+
/// unbounded keys would let one client fill the cache with garbage.
|
|
111
|
+
const VALUE_INTERN_MAX_LEN: usize = 512;
|
|
112
|
+
|
|
113
|
+
/// Headers whose values are effectively enums or per-install constants
|
|
114
|
+
/// (a browser resends the same UA, accept-*, and sec-ch-* on every
|
|
115
|
+
/// request), so caching kills an allocation + copy per header per
|
|
116
|
+
/// request: the env-side analogue of what HPACK does on the wire.
|
|
117
|
+
/// Deliberately absent: `cookie` and `authorization` (per-user
|
|
118
|
+
/// cardinality would churn the cache, and secrets should not outlive
|
|
119
|
+
/// their request in an evict-to-free cache), `referer`/`x-request-id`
|
|
120
|
+
/// and friends (unbounded cardinality).
|
|
121
|
+
const INTERNABLE_VALUES: &[&str] = &[
|
|
122
|
+
"user-agent",
|
|
123
|
+
"accept",
|
|
124
|
+
"accept-encoding",
|
|
125
|
+
"accept-language",
|
|
126
|
+
"cache-control",
|
|
127
|
+
"dnt",
|
|
128
|
+
"origin",
|
|
129
|
+
"pragma",
|
|
130
|
+
"priority",
|
|
131
|
+
"sec-ch-ua",
|
|
132
|
+
"sec-ch-ua-mobile",
|
|
133
|
+
"sec-ch-ua-platform",
|
|
134
|
+
"sec-fetch-dest",
|
|
135
|
+
"sec-fetch-mode",
|
|
136
|
+
"sec-fetch-site",
|
|
137
|
+
"sec-fetch-user",
|
|
138
|
+
"upgrade-insecure-requests",
|
|
139
|
+
"x-requested-with",
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
/// One hosts-cache entry: the frozen SERVER_NAME/SERVER_PORT pair, plus
|
|
143
|
+
/// the frozen full authority ("host[:port]" as sent) used as the
|
|
144
|
+
/// HTTP_HOST value for requests that carry the name in the URI (the h2
|
|
145
|
+
/// :authority pseudo-header) rather than a Host header. Lazily filled:
|
|
146
|
+
/// Host-header entries and the NUL-prefixed socket-fallback entries
|
|
147
|
+
/// never allocate it.
|
|
148
|
+
pub struct HostEntry {
|
|
149
|
+
name: CachedStr,
|
|
150
|
+
port: CachedStr,
|
|
151
|
+
host: Option<CachedStr>,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
impl HostEntry {
|
|
155
|
+
/// Root a fresh entry; None (with nothing left rooted) when the slab
|
|
156
|
+
/// is full.
|
|
157
|
+
fn root(
|
|
158
|
+
slab: &Arc<PinSlab>,
|
|
159
|
+
name: RString,
|
|
160
|
+
port: RString,
|
|
161
|
+
host: Option<RString>,
|
|
162
|
+
) -> Option<Self> {
|
|
163
|
+
let name = CachedStr::root(slab, name)?;
|
|
164
|
+
let port = CachedStr::root(slab, port)?;
|
|
165
|
+
let host = match host {
|
|
166
|
+
Some(host) => Some(CachedStr::root(slab, host)?),
|
|
167
|
+
None => None,
|
|
168
|
+
};
|
|
169
|
+
Some(HostEntry { name, port, host })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn name_port(&self, ruby: &Ruby) -> (RString, RString) {
|
|
173
|
+
(self.name.get(ruby), self.port.get(ruby))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// The entry's full-authority string, adopting `host` (rooted, slab
|
|
177
|
+
/// permitting) when it has none yet.
|
|
178
|
+
fn fill_host(&mut self, ruby: &Ruby, slab: &Arc<PinSlab>, host: RString) -> RString {
|
|
179
|
+
if self.host.is_none() {
|
|
180
|
+
self.host = CachedStr::root(slab, host);
|
|
181
|
+
}
|
|
182
|
+
self.host.as_ref().map_or(host, |cached| cached.get(ruby))
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// A frozen RString kept alive by one slab slot for as long as its cache
|
|
187
|
+
/// entry exists. Drop (LRU eviction, or an insert that lost a race)
|
|
188
|
+
/// clears the slot; the string then dies at the next sweep unless an
|
|
189
|
+
/// env still references it.
|
|
69
190
|
///
|
|
70
|
-
///
|
|
71
|
-
///
|
|
72
|
-
///
|
|
73
|
-
///
|
|
74
|
-
///
|
|
75
|
-
///
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
191
|
+
/// Readers copy the VALUE out under the cache lock and set it on their
|
|
192
|
+
/// env only afterwards (the aset is a Ruby call, so it may not run under
|
|
193
|
+
/// the lock; see the module docs). That is sound even when another
|
|
194
|
+
/// ractor evicts the entry in between: a VALUE held in a native frame is
|
|
195
|
+
/// a conservative GC root, since Ruby scans every thread's machine stack
|
|
196
|
+
/// and saved registers on every ractor, so the string outlives the
|
|
197
|
+
/// window until the env roots it for good.
|
|
198
|
+
pub struct CachedStr {
|
|
199
|
+
value: Opaque<RString>,
|
|
200
|
+
slab: Arc<PinSlab>,
|
|
201
|
+
slot: usize,
|
|
202
|
+
}
|
|
79
203
|
|
|
80
204
|
impl CachedStr {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
string.
|
|
84
|
-
CachedStr
|
|
205
|
+
/// Root `string` in the slab; None when the slab is full.
|
|
206
|
+
fn root(slab: &Arc<PinSlab>, string: RString) -> Option<Self> {
|
|
207
|
+
let slot = slab.insert(string.as_raw())?;
|
|
208
|
+
Some(CachedStr {
|
|
209
|
+
value: Opaque::from(string),
|
|
210
|
+
slab: slab.clone(),
|
|
211
|
+
slot,
|
|
212
|
+
})
|
|
85
213
|
}
|
|
86
214
|
|
|
87
|
-
fn get(&self) -> RString {
|
|
88
|
-
|
|
215
|
+
fn get(&self, ruby: &Ruby) -> RString {
|
|
216
|
+
ruby.get_inner(self.value)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
impl Drop for CachedStr {
|
|
221
|
+
fn drop(&mut self) {
|
|
222
|
+
self.slab.release(self.slot);
|
|
89
223
|
}
|
|
90
224
|
}
|
|
91
225
|
|
|
@@ -142,6 +276,8 @@ const COMMON_HEADERS: &[&str] = &[
|
|
|
142
276
|
"sec-ch-ua-mobile",
|
|
143
277
|
"sec-ch-ua-platform",
|
|
144
278
|
"keep-alive",
|
|
279
|
+
"priority",
|
|
280
|
+
"alt-used",
|
|
145
281
|
];
|
|
146
282
|
|
|
147
283
|
pub fn cgi_name(lower: &str) -> String {
|
|
@@ -156,13 +292,39 @@ pub fn cgi_name(lower: &str) -> String {
|
|
|
156
292
|
key
|
|
157
293
|
}
|
|
158
294
|
|
|
295
|
+
/// A frozen string rooted for the life of the process. Init only: the
|
|
296
|
+
/// registration takes the VM lock, and nothing else must exist yet.
|
|
159
297
|
fn frozen(ruby: &Ruby, s: &str) -> Opaque<RString> {
|
|
160
|
-
let string = ruby
|
|
161
|
-
string.freeze();
|
|
298
|
+
let string = frozen_str(ruby, s);
|
|
162
299
|
gc::register_mark_object(string);
|
|
163
300
|
Opaque::from(string)
|
|
164
301
|
}
|
|
165
302
|
|
|
303
|
+
/// A fresh frozen string, rooted only by the caller's stack until a
|
|
304
|
+
/// cache slot or an env takes it.
|
|
305
|
+
fn frozen_str(ruby: &Ruby, s: &str) -> RString {
|
|
306
|
+
let string = ruby.str_new(s);
|
|
307
|
+
string.freeze();
|
|
308
|
+
string
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// Header values are bytes on the wire (not guaranteed UTF-8), so they
|
|
312
|
+
/// cache as the same binary strings `str_from_slice` builds on the
|
|
313
|
+
/// uncached path; interning must not change the encoding an app
|
|
314
|
+
/// observes.
|
|
315
|
+
fn frozen_slice(ruby: &Ruby, bytes: &[u8]) -> RString {
|
|
316
|
+
let string = ruby.str_from_slice(bytes);
|
|
317
|
+
string.freeze();
|
|
318
|
+
string
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fn lru<K: Hash + Eq, V>(capacity: usize) -> Mutex<LruCache<K, V>> {
|
|
322
|
+
Mutex::new(LruCache::with_hasher(
|
|
323
|
+
std::num::NonZeroUsize::new(capacity).expect("cache capacity is non-zero"),
|
|
324
|
+
ahash::RandomState::new(),
|
|
325
|
+
))
|
|
326
|
+
}
|
|
327
|
+
|
|
166
328
|
/// Build the cache. Main ractor, GVL held, before any worker exists.
|
|
167
329
|
pub fn init(ruby: &Ruby) {
|
|
168
330
|
let methods = COMMON_METHODS
|
|
@@ -174,6 +336,11 @@ pub fn init(ruby: &Ruby) {
|
|
|
174
336
|
.map(|h| (*h, frozen(ruby, &cgi_name(h))))
|
|
175
337
|
.collect::<HashMap<_, _>>();
|
|
176
338
|
|
|
339
|
+
// The slab's GC-visible face lives for the process: registered here
|
|
340
|
+
// on the main ractor, before any worker ractor can exist.
|
|
341
|
+
let slab = Arc::new(PinSlab::with_capacity(CACHE_SLAB_CAPACITY));
|
|
342
|
+
gc::register_mark_object(ruby.obj_wrap(PinKeeper(slab.clone())));
|
|
343
|
+
|
|
177
344
|
let strings = EnvStrings {
|
|
178
345
|
request_method: frozen(ruby, "REQUEST_METHOD"),
|
|
179
346
|
script_name: frozen(ruby, "SCRIPT_NAME"),
|
|
@@ -194,16 +361,14 @@ pub fn init(ruby: &Ruby) {
|
|
|
194
361
|
https: frozen(ruby, "https"),
|
|
195
362
|
http10: frozen(ruby, "HTTP/1.0"),
|
|
196
363
|
http11: frozen(ruby, "HTTP/1.1"),
|
|
364
|
+
http2: frozen(ruby, "HTTP/2"),
|
|
197
365
|
methods,
|
|
198
366
|
header_names,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
std::num::NonZeroUsize::new(ADDR_CACHE_CAP).unwrap(),
|
|
205
|
-
ahash::RandomState::new(),
|
|
206
|
-
)),
|
|
367
|
+
slab,
|
|
368
|
+
hosts: lru(HOST_CACHE_CAP),
|
|
369
|
+
addrs: lru(ADDR_CACHE_CAP),
|
|
370
|
+
values: lru(VALUE_CACHE_CAP),
|
|
371
|
+
internable: INTERNABLE_VALUES.iter().copied().collect(),
|
|
207
372
|
errors_stream: RwLock::new(None),
|
|
208
373
|
null_input: RwLock::new(None),
|
|
209
374
|
};
|
|
@@ -214,18 +379,24 @@ pub fn get() -> &'static EnvStrings {
|
|
|
214
379
|
ENV_STRINGS.get().expect("env_strings::init not called")
|
|
215
380
|
}
|
|
216
381
|
|
|
217
|
-
/// Called once from lib/kino.rb (main ractor) with the
|
|
218
|
-
///
|
|
382
|
+
/// Called once from lib/kino.rb (main ractor) with the Ractor-shareable
|
|
383
|
+
/// singletons the Ruby layer owns. Shareability is the real requirement
|
|
384
|
+
/// (every worker ractor reads them out of its envs), and it is stricter
|
|
385
|
+
/// than frozen: a frozen object whose state is not itself shareable
|
|
386
|
+
/// fails it. `Ractor.shareable?` is `rb_ractor_shareable_p`, which rb-sys
|
|
387
|
+
/// does not bind (it is a static inline).
|
|
219
388
|
pub fn register_defaults(
|
|
220
389
|
ruby: &Ruby,
|
|
221
390
|
errors: Value,
|
|
222
391
|
null_input: Value,
|
|
223
392
|
) -> Result<(), magnus::Error> {
|
|
393
|
+
let ractor: Value = ruby.class_object().const_get("Ractor")?;
|
|
224
394
|
for value in [errors, null_input] {
|
|
225
|
-
|
|
395
|
+
let shareable: bool = ractor.funcall("shareable?", (value,))?;
|
|
396
|
+
if !shareable {
|
|
226
397
|
return Err(magnus::Error::new(
|
|
227
398
|
ruby.exception_arg_error(),
|
|
228
|
-
"register_defaults expects
|
|
399
|
+
"register_defaults expects Ractor-shareable objects",
|
|
229
400
|
));
|
|
230
401
|
}
|
|
231
402
|
}
|
|
@@ -237,9 +408,41 @@ pub fn register_defaults(
|
|
|
237
408
|
Ok(())
|
|
238
409
|
}
|
|
239
410
|
|
|
411
|
+
/// The cached value for `key`, or a freshly built one, inserted for the
|
|
412
|
+
/// next caller. `build` runs with the lock released (it allocates Ruby
|
|
413
|
+
/// strings; see the module docs); `read` copies a cached entry's value
|
|
414
|
+
/// out; `root` turns a fresh value into an entry, or None when the slab
|
|
415
|
+
/// is full, in which case the value is used uncached. An entry a racing
|
|
416
|
+
/// ractor inserted meanwhile wins over ours, whose string is then plain
|
|
417
|
+
/// garbage.
|
|
418
|
+
fn get_or_insert<K, Q, V, T>(
|
|
419
|
+
cache: &Mutex<LruCache<K, V>>,
|
|
420
|
+
key: &Q,
|
|
421
|
+
read: impl Fn(&V) -> T,
|
|
422
|
+
build: impl FnOnce() -> T,
|
|
423
|
+
root: impl FnOnce(&T) -> Option<V>,
|
|
424
|
+
own_key: impl FnOnce() -> K,
|
|
425
|
+
) -> T
|
|
426
|
+
where
|
|
427
|
+
K: Hash + Eq + Borrow<Q>,
|
|
428
|
+
Q: Hash + Eq + ?Sized,
|
|
429
|
+
{
|
|
430
|
+
if let Some(found) = cache.lock().get(key) {
|
|
431
|
+
return read(found);
|
|
432
|
+
}
|
|
433
|
+
let fresh = build();
|
|
434
|
+
let mut cache = cache.lock();
|
|
435
|
+
if let Some(found) = cache.get(key) {
|
|
436
|
+
return read(found);
|
|
437
|
+
}
|
|
438
|
+
if let Some(entry) = root(&fresh) {
|
|
439
|
+
cache.put(own_key(), entry); // may evict: the entry's Drop clears its slots
|
|
440
|
+
}
|
|
441
|
+
fresh
|
|
442
|
+
}
|
|
443
|
+
|
|
240
444
|
/// Set SERVER_NAME/SERVER_PORT on `env` from the LRU host cache, building
|
|
241
|
-
/// (and caching) frozen values on miss.
|
|
242
|
-
/// lock; see CachedStr's safety contract.
|
|
445
|
+
/// (and caching) frozen values on miss.
|
|
243
446
|
pub fn set_host_env(
|
|
244
447
|
ruby: &Ruby,
|
|
245
448
|
env: magnus::RHash,
|
|
@@ -247,43 +450,121 @@ pub fn set_host_env(
|
|
|
247
450
|
make: impl FnOnce() -> (String, u16),
|
|
248
451
|
) -> Result<(), magnus::Error> {
|
|
249
452
|
let s = get();
|
|
250
|
-
let
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
let
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
values
|
|
262
|
-
}
|
|
263
|
-
};
|
|
453
|
+
let (name, port) = get_or_insert(
|
|
454
|
+
&s.hosts,
|
|
455
|
+
host,
|
|
456
|
+
|entry| entry.name_port(ruby),
|
|
457
|
+
|| {
|
|
458
|
+
let (name, port) = make();
|
|
459
|
+
(frozen_str(ruby, &name), frozen_str(ruby, &port.to_string()))
|
|
460
|
+
},
|
|
461
|
+
|&(name, port)| HostEntry::root(&s.slab, name, port, None),
|
|
462
|
+
|| host.to_vec(),
|
|
463
|
+
);
|
|
264
464
|
env.aset(ruby.get_inner(s.server_name), name)?;
|
|
265
465
|
env.aset(ruby.get_inner(s.server_port), port)?;
|
|
266
466
|
Ok(())
|
|
267
467
|
}
|
|
268
468
|
|
|
269
|
-
/// Set
|
|
270
|
-
///
|
|
271
|
-
|
|
469
|
+
/// Set SERVER_NAME/SERVER_PORT *and* HTTP_HOST on `env` from the URI
|
|
470
|
+
/// authority (every h2 request via :authority; also h1 absolute-form).
|
|
471
|
+
/// HTTP_HOST is set here because such requests carry no Host header for
|
|
472
|
+
/// the header loop to surface. Same cache and locking contract as
|
|
473
|
+
/// [`set_host_env`]; an entry first created by a Host header upgrades in
|
|
474
|
+
/// place, gaining the full-authority string on first use.
|
|
475
|
+
pub fn set_authority_env(
|
|
476
|
+
ruby: &Ruby,
|
|
477
|
+
env: magnus::RHash,
|
|
478
|
+
authority: &str,
|
|
479
|
+
make: impl FnOnce() -> (String, u16),
|
|
480
|
+
) -> Result<(), magnus::Error> {
|
|
272
481
|
let s = get();
|
|
273
|
-
let
|
|
274
|
-
let
|
|
275
|
-
|
|
482
|
+
let key = authority.as_bytes();
|
|
483
|
+
let read = |entry: &HostEntry| {
|
|
484
|
+
let (name, port) = entry.name_port(ruby);
|
|
485
|
+
(name, port, entry.host.as_ref().map(|host| host.get(ruby)))
|
|
486
|
+
};
|
|
487
|
+
// Bound to a local so the lock guard (a temporary of this statement)
|
|
488
|
+
// is gone before the arms below take the lock again.
|
|
489
|
+
let hit = s.hosts.lock().get(key).map(read);
|
|
490
|
+
let (name, port, host) = match hit {
|
|
491
|
+
Some((name, port, Some(host))) => (name, port, host),
|
|
492
|
+
Some((name, port, None)) => {
|
|
493
|
+
let host = frozen_str(ruby, authority);
|
|
494
|
+
let mut hosts = s.hosts.lock();
|
|
495
|
+
match hosts.get_mut(key) {
|
|
496
|
+
Some(entry) => (name, port, entry.fill_host(ruby, &s.slab, host)),
|
|
497
|
+
// Evicted meanwhile; the strings in hand are still valid.
|
|
498
|
+
None => (name, port, host),
|
|
499
|
+
}
|
|
500
|
+
}
|
|
276
501
|
None => {
|
|
277
|
-
let
|
|
278
|
-
let
|
|
279
|
-
|
|
280
|
-
|
|
502
|
+
let (name, port) = make();
|
|
503
|
+
let name = frozen_str(ruby, &name);
|
|
504
|
+
let port = frozen_str(ruby, &port.to_string());
|
|
505
|
+
let host = frozen_str(ruby, authority);
|
|
506
|
+
let mut hosts = s.hosts.lock();
|
|
507
|
+
match hosts.get_mut(key) {
|
|
508
|
+
// A racing ractor got there first; share its entry.
|
|
509
|
+
Some(entry) => {
|
|
510
|
+
let (name, port) = entry.name_port(ruby);
|
|
511
|
+
(name, port, entry.fill_host(ruby, &s.slab, host))
|
|
512
|
+
}
|
|
513
|
+
None => {
|
|
514
|
+
if let Some(entry) = HostEntry::root(&s.slab, name, port, Some(host)) {
|
|
515
|
+
hosts.put(key.to_vec(), entry);
|
|
516
|
+
}
|
|
517
|
+
(name, port, host)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
281
520
|
}
|
|
282
521
|
};
|
|
283
|
-
env.aset(ruby.get_inner(s.
|
|
522
|
+
env.aset(ruby.get_inner(s.server_name), name)?;
|
|
523
|
+
env.aset(ruby.get_inner(s.server_port), port)?;
|
|
524
|
+
let host_key = *s.header_names.get("host").expect("host is a common header");
|
|
525
|
+
env.aset(ruby.get_inner(host_key), host)?;
|
|
284
526
|
Ok(())
|
|
285
527
|
}
|
|
286
528
|
|
|
529
|
+
/// Set one header's value on `env` under `key`: through the interned
|
|
530
|
+
/// value cache when the header qualifies (low-cardinality name, bounded
|
|
531
|
+
/// length), else a fresh per-request string.
|
|
532
|
+
pub fn set_value_env(
|
|
533
|
+
ruby: &Ruby,
|
|
534
|
+
env: magnus::RHash,
|
|
535
|
+
key: RString,
|
|
536
|
+
name: &str,
|
|
537
|
+
value: &[u8],
|
|
538
|
+
) -> Result<(), magnus::Error> {
|
|
539
|
+
let s = get();
|
|
540
|
+
if value.len() > VALUE_INTERN_MAX_LEN || !s.internable.contains(name) {
|
|
541
|
+
return env.aset(key, ruby.str_from_slice(value));
|
|
542
|
+
}
|
|
543
|
+
let cached = get_or_insert(
|
|
544
|
+
&s.values,
|
|
545
|
+
value,
|
|
546
|
+
|cached| cached.get(ruby),
|
|
547
|
+
|| frozen_slice(ruby, value),
|
|
548
|
+
|&fresh| CachedStr::root(&s.slab, fresh),
|
|
549
|
+
|| value.to_vec(),
|
|
550
|
+
);
|
|
551
|
+
env.aset(key, cached)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/// Set REMOTE_ADDR on `env` from the LRU peer-IP cache.
|
|
555
|
+
pub fn set_addr_env(ruby: &Ruby, env: magnus::RHash, ip: IpAddr) -> Result<(), magnus::Error> {
|
|
556
|
+
let s = get();
|
|
557
|
+
let value = get_or_insert(
|
|
558
|
+
&s.addrs,
|
|
559
|
+
&ip,
|
|
560
|
+
|cached| cached.get(ruby),
|
|
561
|
+
|| frozen_str(ruby, &ip.to_string()),
|
|
562
|
+
|&fresh| CachedStr::root(&s.slab, fresh),
|
|
563
|
+
|| ip,
|
|
564
|
+
);
|
|
565
|
+
env.aset(ruby.get_inner(s.remote_addr), value)
|
|
566
|
+
}
|
|
567
|
+
|
|
287
568
|
#[cfg(test)]
|
|
288
569
|
mod tests {
|
|
289
570
|
use super::{cgi_name, COMMON_HEADERS, COMMON_METHODS};
|
data/ext/kino/src/lib.rs
CHANGED
|
@@ -41,8 +41,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
41
41
|
let native = module.define_module("Native")?;
|
|
42
42
|
native.define_singleton_method("server_start", function!(server::server_start, 1))?;
|
|
43
43
|
native.define_singleton_method("register_worker", function!(server::register_worker, 1))?;
|
|
44
|
-
native.define_singleton_method("
|
|
45
|
-
native.define_singleton_method("take_batch", function!(queue::take_batch, 3))?;
|
|
44
|
+
native.define_singleton_method("worker", function!(queue::worker, 2))?;
|
|
46
45
|
native.define_singleton_method("stop_accepting", function!(server::stop_accepting, 1))?;
|
|
47
46
|
native.define_singleton_method("close_queue", function!(server::close_queue, 1))?;
|
|
48
47
|
native.define_singleton_method("queue_stats", function!(server::queue_stats, 1))?;
|
|
@@ -50,6 +49,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
50
49
|
native.define_singleton_method("worker_stats", function!(server::worker_stats, 1))?;
|
|
51
50
|
native.define_singleton_method("queue_time", function!(server::queue_time, 1))?;
|
|
52
51
|
native.define_singleton_method("quarantine_slot", function!(server::quarantine_slot, 2))?;
|
|
52
|
+
native.define_singleton_method("retire_slot", function!(server::retire_slot, 2))?;
|
|
53
|
+
native.define_singleton_method("reset_slot", function!(server::reset_slot, 2))?;
|
|
54
|
+
native.define_singleton_method(
|
|
55
|
+
"set_active_workers",
|
|
56
|
+
function!(server::set_active_workers, 2),
|
|
57
|
+
)?;
|
|
58
|
+
native.define_singleton_method("record_scale_up", function!(server::record_scale_up, 1))?;
|
|
59
|
+
native.define_singleton_method("record_scale_down", function!(server::record_scale_down, 1))?;
|
|
60
|
+
native.define_singleton_method("pool_stats", function!(server::pool_stats, 1))?;
|
|
53
61
|
native.define_singleton_method(
|
|
54
62
|
"record_quarantine_replacement",
|
|
55
63
|
function!(server::record_quarantine_replacement, 1),
|
|
@@ -84,11 +92,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
84
92
|
native.define_class("PinKeeper", ruby.class_object())?;
|
|
85
93
|
native.define_singleton_method("pin_keeper", function!(server::pin_keeper, 1))?;
|
|
86
94
|
|
|
95
|
+
let worker = native.define_class("Worker", ruby.class_object())?;
|
|
96
|
+
worker.define_method("take_one", method!(queue::Worker::take_one, 0))?;
|
|
97
|
+
worker.define_method("take_batch", method!(queue::Worker::take_batch, 1))?;
|
|
98
|
+
|
|
87
99
|
let request = native.define_class("Request", ruby.class_object())?;
|
|
88
|
-
request.define_method("respond_and_take", method!(queue::respond_and_take,
|
|
100
|
+
request.define_method("respond_and_take", method!(queue::respond_and_take, 5))?;
|
|
89
101
|
request.define_method(
|
|
90
102
|
"respond_and_take_one",
|
|
91
|
-
method!(queue::respond_and_take_one,
|
|
103
|
+
method!(queue::respond_and_take_one, 4),
|
|
92
104
|
)?;
|
|
93
105
|
request.define_method("read_body", method!(Request::read_body, 1))?;
|
|
94
106
|
request.define_method("send_simple", method!(crate::request::respond_simple, 3))?;
|
|
@@ -98,10 +110,12 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
98
110
|
request.define_method("abort", method!(Request::abort, 0))?;
|
|
99
111
|
request.define_method("timing", method!(Request::set_timing, 2))?;
|
|
100
112
|
|
|
101
|
-
// Force-resolve the TypedData class
|
|
102
|
-
// resolves
|
|
103
|
-
// worker ractors is the failure mode we must rule out.
|
|
113
|
+
// Force-resolve the TypedData class caches on the main ractor: magnus
|
|
114
|
+
// resolves them lazily on first wrap, and a racy first resolution from
|
|
115
|
+
// two worker ractors is the failure mode we must rule out.
|
|
104
116
|
let _ = <Request as magnus::TypedData>::class(ruby);
|
|
117
|
+
let _ = <queue::Worker as magnus::TypedData>::class(ruby);
|
|
118
|
+
let _ = <pin::PinKeeper as magnus::TypedData>::class(ruby);
|
|
105
119
|
|
|
106
120
|
// Frozen env key/value cache: built once here (main ractor, GVL held),
|
|
107
121
|
// shared by every worker ractor afterwards.
|
|
@@ -112,6 +126,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
112
126
|
native.define_singleton_method("_test_take", function!(test_support::take, 1))?;
|
|
113
127
|
native.define_singleton_method("_test_close", function!(test_support::close, 1))?;
|
|
114
128
|
native.define_singleton_method("_test_panic", function!(test_support::panic_in_release, 0))?;
|
|
129
|
+
native.define_singleton_method("_test_env_probe", function!(test_support::env_probe, 4))?;
|
|
115
130
|
|
|
116
131
|
Ok(())
|
|
117
132
|
}
|