kino 0.6.0 → 0.7.1
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 +30 -0
- data/Cargo.lock +64 -71
- data/README.md +52 -0
- data/doc/architecture.md +10 -0
- data/ext/kino/Cargo.toml +2 -2
- data/ext/kino/src/control.rs +103 -6
- data/ext/kino/src/env_strings.rs +267 -124
- data/ext/kino/src/lib.rs +11 -0
- data/ext/kino/src/pin.rs +66 -15
- data/ext/kino/src/queue.rs +99 -3
- data/ext/kino/src/registry.rs +142 -1
- data/ext/kino/src/request.rs +2 -2
- data/ext/kino/src/server.rs +220 -4
- data/ext/kino/src/test_support.rs +44 -0
- data/lib/kino/cli.rb +12 -4
- data/lib/kino/configuration.rb +16 -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 +64 -85
- data/lib/kino/slot_bank.rb +31 -0
- data/lib/kino/templates/kino.rb.tt +15 -0
- data/lib/kino/threaded_pool.rb +186 -0
- data/lib/kino/version.rb +1 -1
- data/lib/kino.rb +4 -0
- data/lib/rackup/handler/kino.rb +4 -4
- data/sig/kino.rbs +2 -0
- metadata +6 -2
data/ext/kino/src/env_strings.rs
CHANGED
|
@@ -8,20 +8,44 @@
|
|
|
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
|
+
//! Every cached string is flagged shareable as it is built, not just
|
|
16
|
+
//! frozen: under per-ractor GC (Ruby 4.1) that flag is what keeps a
|
|
17
|
+
//! string one ractor allocated alive in another ractor's env.
|
|
18
|
+
//!
|
|
19
|
+
//! The LRU caches (hosts, peer addresses, interned header values) are the
|
|
20
|
+
//! exception: they insert and evict from every worker ractor in parallel.
|
|
21
|
+
//! That rules out per-value GC registration, because in Ruby 4.0
|
|
22
|
+
//! `rb_gc_register_address` and its unregister walk a VM-wide linked list
|
|
23
|
+
//! with no lock. Their strings are rooted through a [`PinSlab`] instead:
|
|
24
|
+
//! one atomic VALUE slot per cached string, marked from a keeper object
|
|
25
|
+
//! registered once at init. The same parallelism forbids any Ruby call
|
|
26
|
+
//! while a cache lock is held: a Ruby allocation can start a GC, a GC
|
|
27
|
+
//! with several ractors stops at a barrier every other ractor must
|
|
28
|
+
//! reach, and a ractor parked on the cache mutex never would. So the
|
|
29
|
+
//! caches allocate their strings first, then lock only to look up, root,
|
|
30
|
+
//! insert, or evict.
|
|
13
31
|
|
|
32
|
+
use std::borrow::Borrow;
|
|
33
|
+
use std::hash::Hash;
|
|
14
34
|
use std::net::IpAddr;
|
|
15
|
-
use std::sync::OnceLock;
|
|
35
|
+
use std::sync::{Arc, OnceLock};
|
|
16
36
|
|
|
37
|
+
use magnus::rb_sys::AsRawValue;
|
|
17
38
|
use magnus::value::Opaque;
|
|
18
|
-
use magnus::{
|
|
39
|
+
use magnus::{prelude::*, RString, Ruby, Value};
|
|
19
40
|
use parking_lot::{Mutex, RwLock};
|
|
20
41
|
|
|
42
|
+
use crate::pin::{PinKeeper, PinSlab};
|
|
43
|
+
|
|
21
44
|
/// These maps are probed several times per request; ahash beats the
|
|
22
45
|
/// DoS-resistant default since every key here is our own static data.
|
|
23
46
|
type HashMap<K, V> = std::collections::HashMap<K, V, ahash::RandomState>;
|
|
24
47
|
type HashSet<K> = std::collections::HashSet<K, ahash::RandomState>;
|
|
48
|
+
type LruCache<K, V> = lru::LruCache<K, V, ahash::RandomState>;
|
|
25
49
|
|
|
26
50
|
pub struct EnvStrings {
|
|
27
51
|
// keys
|
|
@@ -49,19 +73,22 @@ pub struct EnvStrings {
|
|
|
49
73
|
pub methods: HashMap<&'static str, Opaque<RString>>,
|
|
50
74
|
/// lowercase header name -> frozen "HTTP_<UPPER>" key
|
|
51
75
|
pub header_names: HashMap<&'static str, Opaque<RString>>,
|
|
76
|
+
/// GC roots for every string the LRU caches below hold (see the
|
|
77
|
+
/// module docs); a cache entry owns its slots and clears them when
|
|
78
|
+
/// it is evicted.
|
|
79
|
+
pub slab: Arc<PinSlab>,
|
|
52
80
|
/// Host-header or :authority bytes -> frozen host values, and peer
|
|
53
81
|
/// IP -> frozen REMOTE_ADDR value. Real traffic has low cardinality
|
|
54
82
|
/// on both, so these kill 3 string allocations per request.
|
|
55
|
-
/// LRU-bounded
|
|
56
|
-
///
|
|
57
|
-
|
|
58
|
-
pub
|
|
59
|
-
pub addrs: Mutex<lru::LruCache<IpAddr, CachedStr, ahash::RandomState>>,
|
|
83
|
+
/// LRU-bounded, so a rotating-host attack recycles cache slots
|
|
84
|
+
/// instead of accumulating immortal strings.
|
|
85
|
+
pub hosts: Mutex<LruCache<Vec<u8>, HostEntry>>,
|
|
86
|
+
pub addrs: Mutex<LruCache<IpAddr, CachedStr>>,
|
|
60
87
|
/// Interned values of low-cardinality headers (see
|
|
61
88
|
/// [`INTERNABLE_VALUES`]): value bytes -> frozen RString, shared
|
|
62
89
|
/// across headers that happen to carry the same bytes. Same rooting
|
|
63
90
|
/// and locking contract as `hosts`/`addrs`.
|
|
64
|
-
pub values: Mutex<
|
|
91
|
+
pub values: Mutex<LruCache<Vec<u8>, CachedStr>>,
|
|
65
92
|
/// The names whose values go through the `values` cache.
|
|
66
93
|
pub internable: HashSet<&'static str>,
|
|
67
94
|
/// Ractor-shareable defaults provided by the Ruby layer at boot:
|
|
@@ -74,6 +101,14 @@ const HOST_CACHE_CAP: usize = 256;
|
|
|
74
101
|
const ADDR_CACHE_CAP: usize = 1024;
|
|
75
102
|
const VALUE_CACHE_CAP: usize = 512;
|
|
76
103
|
|
|
104
|
+
/// Slab slots for every string the three caches can hold at once: up to
|
|
105
|
+
/// three per host entry, one per address and per interned value, plus
|
|
106
|
+
/// one entry per cache for the insert that precedes an eviction. A full
|
|
107
|
+
/// slab (which this bound rules out) degrades to an uncached string,
|
|
108
|
+
/// never to an unrooted one.
|
|
109
|
+
const CACHE_SLAB_CAPACITY: usize =
|
|
110
|
+
(HOST_CACHE_CAP + 1) * 3 + (ADDR_CACHE_CAP + 1) + (VALUE_CACHE_CAP + 1);
|
|
111
|
+
|
|
77
112
|
/// Values longer than this are never interned: past it the memcpy into
|
|
78
113
|
/// a fresh Ruby string is cheap relative to the bytes themselves, and
|
|
79
114
|
/// unbounded keys would let one client fill the cache with garbage.
|
|
@@ -120,38 +155,75 @@ pub struct HostEntry {
|
|
|
120
155
|
host: Option<CachedStr>,
|
|
121
156
|
}
|
|
122
157
|
|
|
123
|
-
|
|
124
|
-
///
|
|
158
|
+
impl HostEntry {
|
|
159
|
+
/// Root a fresh entry; None (with nothing left rooted) when the slab
|
|
160
|
+
/// is full.
|
|
161
|
+
fn root(
|
|
162
|
+
slab: &Arc<PinSlab>,
|
|
163
|
+
name: RString,
|
|
164
|
+
port: RString,
|
|
165
|
+
host: Option<RString>,
|
|
166
|
+
) -> Option<Self> {
|
|
167
|
+
let name = CachedStr::root(slab, name)?;
|
|
168
|
+
let port = CachedStr::root(slab, port)?;
|
|
169
|
+
let host = match host {
|
|
170
|
+
Some(host) => Some(CachedStr::root(slab, host)?),
|
|
171
|
+
None => None,
|
|
172
|
+
};
|
|
173
|
+
Some(HostEntry { name, port, host })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn name_port(&self, ruby: &Ruby) -> (RString, RString) {
|
|
177
|
+
(self.name.get(ruby), self.port.get(ruby))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// The entry's full-authority string, adopting `host` (rooted, slab
|
|
181
|
+
/// permitting) when it has none yet.
|
|
182
|
+
fn fill_host(&mut self, ruby: &Ruby, slab: &Arc<PinSlab>, host: RString) -> RString {
|
|
183
|
+
if self.host.is_none() {
|
|
184
|
+
self.host = CachedStr::root(slab, host);
|
|
185
|
+
}
|
|
186
|
+
self.host.as_ref().map_or(host, |cached| cached.get(ruby))
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// A frozen RString kept alive by one slab slot for as long as its cache
|
|
191
|
+
/// entry exists. Drop (LRU eviction, or an insert that lost a race)
|
|
192
|
+
/// clears the slot; the string then dies at the next sweep unless an
|
|
193
|
+
/// env still references it.
|
|
125
194
|
///
|
|
126
|
-
///
|
|
127
|
-
///
|
|
128
|
-
///
|
|
129
|
-
///
|
|
130
|
-
///
|
|
131
|
-
///
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
195
|
+
/// Readers copy the VALUE out under the cache lock and set it on their
|
|
196
|
+
/// env only afterwards (the aset is a Ruby call, so it may not run under
|
|
197
|
+
/// the lock; see the module docs). That is sound even when another
|
|
198
|
+
/// ractor evicts the entry in between: a VALUE held in a native frame is
|
|
199
|
+
/// a conservative GC root, since Ruby scans every thread's machine stack
|
|
200
|
+
/// and saved registers on every ractor, so the string outlives the
|
|
201
|
+
/// window until the env roots it for good.
|
|
202
|
+
pub struct CachedStr {
|
|
203
|
+
value: Opaque<RString>,
|
|
204
|
+
slab: Arc<PinSlab>,
|
|
205
|
+
slot: usize,
|
|
206
|
+
}
|
|
135
207
|
|
|
136
208
|
impl CachedStr {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
string.
|
|
140
|
-
CachedStr
|
|
209
|
+
/// Root `string` in the slab; None when the slab is full.
|
|
210
|
+
fn root(slab: &Arc<PinSlab>, string: RString) -> Option<Self> {
|
|
211
|
+
let slot = slab.insert(string.as_raw())?;
|
|
212
|
+
Some(CachedStr {
|
|
213
|
+
value: Opaque::from(string),
|
|
214
|
+
slab: slab.clone(),
|
|
215
|
+
slot,
|
|
216
|
+
})
|
|
141
217
|
}
|
|
142
218
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
/// the uncached path; interning must not change the encoding an
|
|
146
|
-
/// app observes.
|
|
147
|
-
fn new_from_slice(ruby: &Ruby, bytes: &[u8]) -> Self {
|
|
148
|
-
let string = ruby.str_from_slice(bytes);
|
|
149
|
-
string.freeze();
|
|
150
|
-
CachedStr(magnus::value::BoxValue::new(string))
|
|
219
|
+
fn get(&self, ruby: &Ruby) -> RString {
|
|
220
|
+
ruby.get_inner(self.value)
|
|
151
221
|
}
|
|
222
|
+
}
|
|
152
223
|
|
|
153
|
-
|
|
154
|
-
|
|
224
|
+
impl Drop for CachedStr {
|
|
225
|
+
fn drop(&mut self) {
|
|
226
|
+
self.slab.release(self.slot);
|
|
155
227
|
}
|
|
156
228
|
}
|
|
157
229
|
|
|
@@ -224,13 +296,49 @@ pub fn cgi_name(lower: &str) -> String {
|
|
|
224
296
|
key
|
|
225
297
|
}
|
|
226
298
|
|
|
299
|
+
/// A frozen string rooted for the life of the process. Init only: the
|
|
300
|
+
/// registration takes the VM lock, and nothing else must exist yet.
|
|
227
301
|
fn frozen(ruby: &Ruby, s: &str) -> Opaque<RString> {
|
|
228
|
-
let string = ruby
|
|
229
|
-
|
|
230
|
-
gc::register_mark_object(string);
|
|
302
|
+
let string = frozen_str(ruby, s);
|
|
303
|
+
ruby.gc_register_mark_object(string);
|
|
231
304
|
Opaque::from(string)
|
|
232
305
|
}
|
|
233
306
|
|
|
307
|
+
/// A fresh frozen string, rooted only by the caller's stack until a
|
|
308
|
+
/// cache slot or an env takes it.
|
|
309
|
+
fn frozen_str(ruby: &Ruby, s: &str) -> RString {
|
|
310
|
+
shareable(ruby.str_new(s))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// Header values are bytes on the wire (not guaranteed UTF-8), so they
|
|
314
|
+
/// cache as the same binary strings `str_from_slice` builds on the
|
|
315
|
+
/// uncached path; interning must not change the encoding an app
|
|
316
|
+
/// observes.
|
|
317
|
+
fn frozen_slice(ruby: &Ruby, bytes: &[u8]) -> RString {
|
|
318
|
+
shareable(ruby.str_from_slice(bytes))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Freeze `string` and flag it shareable. Every cached string ends up
|
|
322
|
+
/// in envs on other ractors, and with per-ractor GC (Ruby 4.1) the flag
|
|
323
|
+
/// is what keeps it alive there: a ractor's local sweep never frees a
|
|
324
|
+
/// shareable object, while an unflagged one allocated on a worker
|
|
325
|
+
/// ractor is rooted only by the keeper on main, which that local GC
|
|
326
|
+
/// does not see. Called before any cache lock is taken (module docs).
|
|
327
|
+
fn shareable(string: RString) -> RString {
|
|
328
|
+
string.freeze();
|
|
329
|
+
// SAFETY: GVL held; a frozen string without ivars is shareable by
|
|
330
|
+
// definition, so this only sets the flag and cannot raise.
|
|
331
|
+
unsafe { rb_sys::rb_ractor_make_shareable(string.as_raw()) };
|
|
332
|
+
string
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fn lru<K: Hash + Eq, V>(capacity: usize) -> Mutex<LruCache<K, V>> {
|
|
336
|
+
Mutex::new(LruCache::with_hasher(
|
|
337
|
+
std::num::NonZeroUsize::new(capacity).expect("cache capacity is non-zero"),
|
|
338
|
+
ahash::RandomState::new(),
|
|
339
|
+
))
|
|
340
|
+
}
|
|
341
|
+
|
|
234
342
|
/// Build the cache. Main ractor, GVL held, before any worker exists.
|
|
235
343
|
pub fn init(ruby: &Ruby) {
|
|
236
344
|
let methods = COMMON_METHODS
|
|
@@ -242,6 +350,11 @@ pub fn init(ruby: &Ruby) {
|
|
|
242
350
|
.map(|h| (*h, frozen(ruby, &cgi_name(h))))
|
|
243
351
|
.collect::<HashMap<_, _>>();
|
|
244
352
|
|
|
353
|
+
// The slab's GC-visible face lives for the process: registered here
|
|
354
|
+
// on the main ractor, before any worker ractor can exist.
|
|
355
|
+
let slab = Arc::new(PinSlab::with_capacity(CACHE_SLAB_CAPACITY));
|
|
356
|
+
ruby.gc_register_mark_object(ruby.obj_wrap(PinKeeper(slab.clone())));
|
|
357
|
+
|
|
245
358
|
let strings = EnvStrings {
|
|
246
359
|
request_method: frozen(ruby, "REQUEST_METHOD"),
|
|
247
360
|
script_name: frozen(ruby, "SCRIPT_NAME"),
|
|
@@ -265,18 +378,10 @@ pub fn init(ruby: &Ruby) {
|
|
|
265
378
|
http2: frozen(ruby, "HTTP/2"),
|
|
266
379
|
methods,
|
|
267
380
|
header_names,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
addrs: Mutex::new(lru::LruCache::with_hasher(
|
|
273
|
-
std::num::NonZeroUsize::new(ADDR_CACHE_CAP).unwrap(),
|
|
274
|
-
ahash::RandomState::new(),
|
|
275
|
-
)),
|
|
276
|
-
values: Mutex::new(lru::LruCache::with_hasher(
|
|
277
|
-
std::num::NonZeroUsize::new(VALUE_CACHE_CAP).unwrap(),
|
|
278
|
-
ahash::RandomState::new(),
|
|
279
|
-
)),
|
|
381
|
+
slab,
|
|
382
|
+
hosts: lru(HOST_CACHE_CAP),
|
|
383
|
+
addrs: lru(ADDR_CACHE_CAP),
|
|
384
|
+
values: lru(VALUE_CACHE_CAP),
|
|
280
385
|
internable: INTERNABLE_VALUES.iter().copied().collect(),
|
|
281
386
|
errors_stream: RwLock::new(None),
|
|
282
387
|
null_input: RwLock::new(None),
|
|
@@ -288,32 +393,70 @@ pub fn get() -> &'static EnvStrings {
|
|
|
288
393
|
ENV_STRINGS.get().expect("env_strings::init not called")
|
|
289
394
|
}
|
|
290
395
|
|
|
291
|
-
/// Called once from lib/kino.rb (main ractor) with the
|
|
292
|
-
///
|
|
396
|
+
/// Called once from lib/kino.rb (main ractor) with the Ractor-shareable
|
|
397
|
+
/// singletons the Ruby layer owns. Shareability is the real requirement
|
|
398
|
+
/// (every worker ractor reads them out of its envs), and it is stricter
|
|
399
|
+
/// than frozen: a frozen object whose state is not itself shareable
|
|
400
|
+
/// fails it. `Ractor.shareable?` is `rb_ractor_shareable_p`, which rb-sys
|
|
401
|
+
/// does not bind (it is a static inline).
|
|
293
402
|
pub fn register_defaults(
|
|
294
403
|
ruby: &Ruby,
|
|
295
404
|
errors: Value,
|
|
296
405
|
null_input: Value,
|
|
297
406
|
) -> Result<(), magnus::Error> {
|
|
407
|
+
let ractor: Value = ruby.class_object().const_get("Ractor")?;
|
|
298
408
|
for value in [errors, null_input] {
|
|
299
|
-
|
|
409
|
+
let shareable: bool = ractor.funcall("shareable?", (value,))?;
|
|
410
|
+
if !shareable {
|
|
300
411
|
return Err(magnus::Error::new(
|
|
301
412
|
ruby.exception_arg_error(),
|
|
302
|
-
"register_defaults expects
|
|
413
|
+
"register_defaults expects Ractor-shareable objects",
|
|
303
414
|
));
|
|
304
415
|
}
|
|
305
416
|
}
|
|
306
|
-
|
|
307
|
-
|
|
417
|
+
ruby.gc_register_mark_object(errors);
|
|
418
|
+
ruby.gc_register_mark_object(null_input);
|
|
308
419
|
let s = get();
|
|
309
420
|
*s.errors_stream.write() = Some(Opaque::from(errors));
|
|
310
421
|
*s.null_input.write() = Some(Opaque::from(null_input));
|
|
311
422
|
Ok(())
|
|
312
423
|
}
|
|
313
424
|
|
|
425
|
+
/// The cached value for `key`, or a freshly built one, inserted for the
|
|
426
|
+
/// next caller. `build` runs with the lock released (it allocates Ruby
|
|
427
|
+
/// strings; see the module docs); `read` copies a cached entry's value
|
|
428
|
+
/// out; `root` turns a fresh value into an entry, or None when the slab
|
|
429
|
+
/// is full, in which case the value is used uncached. An entry a racing
|
|
430
|
+
/// ractor inserted meanwhile wins over ours, whose string is then plain
|
|
431
|
+
/// garbage.
|
|
432
|
+
fn get_or_insert<K, Q, V, T>(
|
|
433
|
+
cache: &Mutex<LruCache<K, V>>,
|
|
434
|
+
key: &Q,
|
|
435
|
+
read: impl Fn(&V) -> T,
|
|
436
|
+
build: impl FnOnce() -> T,
|
|
437
|
+
root: impl FnOnce(&T) -> Option<V>,
|
|
438
|
+
own_key: impl FnOnce() -> K,
|
|
439
|
+
) -> T
|
|
440
|
+
where
|
|
441
|
+
K: Hash + Eq + Borrow<Q>,
|
|
442
|
+
Q: Hash + Eq + ?Sized,
|
|
443
|
+
{
|
|
444
|
+
if let Some(found) = cache.lock().get(key) {
|
|
445
|
+
return read(found);
|
|
446
|
+
}
|
|
447
|
+
let fresh = build();
|
|
448
|
+
let mut cache = cache.lock();
|
|
449
|
+
if let Some(found) = cache.get(key) {
|
|
450
|
+
return read(found);
|
|
451
|
+
}
|
|
452
|
+
if let Some(entry) = root(&fresh) {
|
|
453
|
+
cache.put(own_key(), entry); // may evict: the entry's Drop clears its slots
|
|
454
|
+
}
|
|
455
|
+
fresh
|
|
456
|
+
}
|
|
457
|
+
|
|
314
458
|
/// Set SERVER_NAME/SERVER_PORT on `env` from the LRU host cache, building
|
|
315
|
-
/// (and caching) frozen values on miss.
|
|
316
|
-
/// lock; see CachedStr's safety contract.
|
|
459
|
+
/// (and caching) frozen values on miss.
|
|
317
460
|
pub fn set_host_env(
|
|
318
461
|
ruby: &Ruby,
|
|
319
462
|
env: magnus::RHash,
|
|
@@ -321,21 +464,17 @@ pub fn set_host_env(
|
|
|
321
464
|
make: impl FnOnce() -> (String, u16),
|
|
322
465
|
) -> Result<(), magnus::Error> {
|
|
323
466
|
let s = get();
|
|
324
|
-
let
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
let
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
hosts.put(host.to_vec(), entry); // may evict + free an old entry
|
|
336
|
-
values
|
|
337
|
-
}
|
|
338
|
-
};
|
|
467
|
+
let (name, port) = get_or_insert(
|
|
468
|
+
&s.hosts,
|
|
469
|
+
host,
|
|
470
|
+
|entry| entry.name_port(ruby),
|
|
471
|
+
|| {
|
|
472
|
+
let (name, port) = make();
|
|
473
|
+
(frozen_str(ruby, &name), frozen_str(ruby, &port.to_string()))
|
|
474
|
+
},
|
|
475
|
+
|&(name, port)| HostEntry::root(&s.slab, name, port, None),
|
|
476
|
+
|| host.to_vec(),
|
|
477
|
+
);
|
|
339
478
|
env.aset(ruby.get_inner(s.server_name), name)?;
|
|
340
479
|
env.aset(ruby.get_inner(s.server_port), port)?;
|
|
341
480
|
Ok(())
|
|
@@ -354,32 +493,44 @@ pub fn set_authority_env(
|
|
|
354
493
|
make: impl FnOnce() -> (String, u16),
|
|
355
494
|
) -> Result<(), magnus::Error> {
|
|
356
495
|
let s = get();
|
|
357
|
-
let
|
|
358
|
-
let
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
496
|
+
let key = authority.as_bytes();
|
|
497
|
+
let read = |entry: &HostEntry| {
|
|
498
|
+
let (name, port) = entry.name_port(ruby);
|
|
499
|
+
(name, port, entry.host.as_ref().map(|host| host.get(ruby)))
|
|
500
|
+
};
|
|
501
|
+
// Bound to a local so the lock guard (a temporary of this statement)
|
|
502
|
+
// is gone before the arms below take the lock again.
|
|
503
|
+
let hit = s.hosts.lock().get(key).map(read);
|
|
504
|
+
let (name, port, host) = match hit {
|
|
505
|
+
Some((name, port, Some(host))) => (name, port, host),
|
|
506
|
+
Some((name, port, None)) => {
|
|
507
|
+
let host = frozen_str(ruby, authority);
|
|
508
|
+
let mut hosts = s.hosts.lock();
|
|
509
|
+
match hosts.get_mut(key) {
|
|
510
|
+
Some(entry) => (name, port, entry.fill_host(ruby, &s.slab, host)),
|
|
511
|
+
// Evicted meanwhile; the strings in hand are still valid.
|
|
512
|
+
None => (name, port, host),
|
|
362
513
|
}
|
|
363
|
-
(
|
|
364
|
-
entry.name.get(),
|
|
365
|
-
entry.port.get(),
|
|
366
|
-
entry.host.as_ref().expect("just filled").get(),
|
|
367
|
-
)
|
|
368
514
|
}
|
|
369
515
|
None => {
|
|
370
|
-
let (
|
|
371
|
-
let
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
entry
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
516
|
+
let (name, port) = make();
|
|
517
|
+
let name = frozen_str(ruby, &name);
|
|
518
|
+
let port = frozen_str(ruby, &port.to_string());
|
|
519
|
+
let host = frozen_str(ruby, authority);
|
|
520
|
+
let mut hosts = s.hosts.lock();
|
|
521
|
+
match hosts.get_mut(key) {
|
|
522
|
+
// A racing ractor got there first; share its entry.
|
|
523
|
+
Some(entry) => {
|
|
524
|
+
let (name, port) = entry.name_port(ruby);
|
|
525
|
+
(name, port, entry.fill_host(ruby, &s.slab, host))
|
|
526
|
+
}
|
|
527
|
+
None => {
|
|
528
|
+
if let Some(entry) = HostEntry::root(&s.slab, name, port, Some(host)) {
|
|
529
|
+
hosts.put(key.to_vec(), entry);
|
|
530
|
+
}
|
|
531
|
+
(name, port, host)
|
|
532
|
+
}
|
|
533
|
+
}
|
|
383
534
|
}
|
|
384
535
|
};
|
|
385
536
|
env.aset(ruby.get_inner(s.server_name), name)?;
|
|
@@ -391,8 +542,7 @@ pub fn set_authority_env(
|
|
|
391
542
|
|
|
392
543
|
/// Set one header's value on `env` under `key`: through the interned
|
|
393
544
|
/// value cache when the header qualifies (low-cardinality name, bounded
|
|
394
|
-
/// length), else a fresh per-request string.
|
|
395
|
-
/// under the cache lock; see CachedStr's safety contract.
|
|
545
|
+
/// length), else a fresh per-request string.
|
|
396
546
|
pub fn set_value_env(
|
|
397
547
|
ruby: &Ruby,
|
|
398
548
|
env: magnus::RHash,
|
|
@@ -404,36 +554,29 @@ pub fn set_value_env(
|
|
|
404
554
|
if value.len() > VALUE_INTERN_MAX_LEN || !s.internable.contains(name) {
|
|
405
555
|
return env.aset(key, ruby.str_from_slice(value));
|
|
406
556
|
}
|
|
407
|
-
let
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
};
|
|
417
|
-
env.aset(key, cached)?;
|
|
418
|
-
Ok(())
|
|
557
|
+
let cached = get_or_insert(
|
|
558
|
+
&s.values,
|
|
559
|
+
value,
|
|
560
|
+
|cached| cached.get(ruby),
|
|
561
|
+
|| frozen_slice(ruby, value),
|
|
562
|
+
|&fresh| CachedStr::root(&s.slab, fresh),
|
|
563
|
+
|| value.to_vec(),
|
|
564
|
+
);
|
|
565
|
+
env.aset(key, cached)
|
|
419
566
|
}
|
|
420
567
|
|
|
421
|
-
/// Set REMOTE_ADDR on `env` from the LRU peer-IP cache
|
|
422
|
-
/// contract as set_host_env.
|
|
568
|
+
/// Set REMOTE_ADDR on `env` from the LRU peer-IP cache.
|
|
423
569
|
pub fn set_addr_env(ruby: &Ruby, env: magnus::RHash, ip: IpAddr) -> Result<(), magnus::Error> {
|
|
424
570
|
let s = get();
|
|
425
|
-
let
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
};
|
|
435
|
-
env.aset(ruby.get_inner(s.remote_addr), value)?;
|
|
436
|
-
Ok(())
|
|
571
|
+
let value = get_or_insert(
|
|
572
|
+
&s.addrs,
|
|
573
|
+
&ip,
|
|
574
|
+
|cached| cached.get(ruby),
|
|
575
|
+
|| frozen_str(ruby, &ip.to_string()),
|
|
576
|
+
|&fresh| CachedStr::root(&s.slab, fresh),
|
|
577
|
+
|| ip,
|
|
578
|
+
);
|
|
579
|
+
env.aset(ruby.get_inner(s.remote_addr), value)
|
|
437
580
|
}
|
|
438
581
|
|
|
439
582
|
#[cfg(test)]
|
data/ext/kino/src/lib.rs
CHANGED
|
@@ -49,6 +49,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
49
49
|
native.define_singleton_method("worker_stats", function!(server::worker_stats, 1))?;
|
|
50
50
|
native.define_singleton_method("queue_time", function!(server::queue_time, 1))?;
|
|
51
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))?;
|
|
52
61
|
native.define_singleton_method(
|
|
53
62
|
"record_quarantine_replacement",
|
|
54
63
|
function!(server::record_quarantine_replacement, 1),
|
|
@@ -106,6 +115,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
106
115
|
// two worker ractors is the failure mode we must rule out.
|
|
107
116
|
let _ = <Request as magnus::TypedData>::class(ruby);
|
|
108
117
|
let _ = <queue::Worker as magnus::TypedData>::class(ruby);
|
|
118
|
+
let _ = <pin::PinKeeper as magnus::TypedData>::class(ruby);
|
|
109
119
|
|
|
110
120
|
// Frozen env key/value cache: built once here (main ractor, GVL held),
|
|
111
121
|
// shared by every worker ractor afterwards.
|
|
@@ -116,6 +126,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
116
126
|
native.define_singleton_method("_test_take", function!(test_support::take, 1))?;
|
|
117
127
|
native.define_singleton_method("_test_close", function!(test_support::close, 1))?;
|
|
118
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))?;
|
|
119
130
|
|
|
120
131
|
Ok(())
|
|
121
132
|
}
|