kino 0.6.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 +21 -0
- data/Cargo.lock +1 -1
- data/README.md +52 -0
- data/doc/architecture.md +10 -0
- data/ext/kino/Cargo.toml +1 -1
- data/ext/kino/src/control.rs +103 -6
- data/ext/kino/src/env_strings.rs +249 -120
- data/ext/kino/src/lib.rs +11 -0
- data/ext/kino/src/pin.rs +21 -9
- data/ext/kino/src/queue.rs +99 -3
- data/ext/kino/src/registry.rs +141 -0
- data/ext/kino/src/request.rs +1 -1
- data/ext/kino/src/server.rs +218 -3
- 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 +5 -1
data/ext/kino/src/env_strings.rs
CHANGED
|
@@ -8,20 +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>;
|
|
24
43
|
type HashSet<K> = std::collections::HashSet<K, ahash::RandomState>;
|
|
44
|
+
type LruCache<K, V> = lru::LruCache<K, V, ahash::RandomState>;
|
|
25
45
|
|
|
26
46
|
pub struct EnvStrings {
|
|
27
47
|
// keys
|
|
@@ -49,19 +69,22 @@ pub struct EnvStrings {
|
|
|
49
69
|
pub methods: HashMap<&'static str, Opaque<RString>>,
|
|
50
70
|
/// lowercase header name -> frozen "HTTP_<UPPER>" key
|
|
51
71
|
pub header_names: HashMap<&'static str, Opaque<RString>>,
|
|
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>,
|
|
52
76
|
/// Host-header or :authority bytes -> frozen host values, and peer
|
|
53
77
|
/// IP -> frozen REMOTE_ADDR value. Real traffic has low cardinality
|
|
54
78
|
/// 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>>,
|
|
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>>,
|
|
60
83
|
/// Interned values of low-cardinality headers (see
|
|
61
84
|
/// [`INTERNABLE_VALUES`]): value bytes -> frozen RString, shared
|
|
62
85
|
/// across headers that happen to carry the same bytes. Same rooting
|
|
63
86
|
/// and locking contract as `hosts`/`addrs`.
|
|
64
|
-
pub values: Mutex<
|
|
87
|
+
pub values: Mutex<LruCache<Vec<u8>, CachedStr>>,
|
|
65
88
|
/// The names whose values go through the `values` cache.
|
|
66
89
|
pub internable: HashSet<&'static str>,
|
|
67
90
|
/// Ractor-shareable defaults provided by the Ruby layer at boot:
|
|
@@ -74,6 +97,14 @@ const HOST_CACHE_CAP: usize = 256;
|
|
|
74
97
|
const ADDR_CACHE_CAP: usize = 1024;
|
|
75
98
|
const VALUE_CACHE_CAP: usize = 512;
|
|
76
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);
|
|
107
|
+
|
|
77
108
|
/// Values longer than this are never interned: past it the memcpy into
|
|
78
109
|
/// a fresh Ruby string is cheap relative to the bytes themselves, and
|
|
79
110
|
/// unbounded keys would let one client fill the cache with garbage.
|
|
@@ -120,38 +151,75 @@ pub struct HostEntry {
|
|
|
120
151
|
host: Option<CachedStr>,
|
|
121
152
|
}
|
|
122
153
|
|
|
123
|
-
|
|
124
|
-
///
|
|
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.
|
|
125
190
|
///
|
|
126
|
-
///
|
|
127
|
-
///
|
|
128
|
-
///
|
|
129
|
-
///
|
|
130
|
-
///
|
|
131
|
-
///
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
}
|
|
135
203
|
|
|
136
204
|
impl CachedStr {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
string.
|
|
140
|
-
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
|
+
})
|
|
141
213
|
}
|
|
142
214
|
|
|
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))
|
|
215
|
+
fn get(&self, ruby: &Ruby) -> RString {
|
|
216
|
+
ruby.get_inner(self.value)
|
|
151
217
|
}
|
|
218
|
+
}
|
|
152
219
|
|
|
153
|
-
|
|
154
|
-
|
|
220
|
+
impl Drop for CachedStr {
|
|
221
|
+
fn drop(&mut self) {
|
|
222
|
+
self.slab.release(self.slot);
|
|
155
223
|
}
|
|
156
224
|
}
|
|
157
225
|
|
|
@@ -224,13 +292,39 @@ pub fn cgi_name(lower: &str) -> String {
|
|
|
224
292
|
key
|
|
225
293
|
}
|
|
226
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.
|
|
227
297
|
fn frozen(ruby: &Ruby, s: &str) -> Opaque<RString> {
|
|
228
|
-
let string = ruby
|
|
229
|
-
string.freeze();
|
|
298
|
+
let string = frozen_str(ruby, s);
|
|
230
299
|
gc::register_mark_object(string);
|
|
231
300
|
Opaque::from(string)
|
|
232
301
|
}
|
|
233
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
|
+
|
|
234
328
|
/// Build the cache. Main ractor, GVL held, before any worker exists.
|
|
235
329
|
pub fn init(ruby: &Ruby) {
|
|
236
330
|
let methods = COMMON_METHODS
|
|
@@ -242,6 +336,11 @@ pub fn init(ruby: &Ruby) {
|
|
|
242
336
|
.map(|h| (*h, frozen(ruby, &cgi_name(h))))
|
|
243
337
|
.collect::<HashMap<_, _>>();
|
|
244
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
|
+
|
|
245
344
|
let strings = EnvStrings {
|
|
246
345
|
request_method: frozen(ruby, "REQUEST_METHOD"),
|
|
247
346
|
script_name: frozen(ruby, "SCRIPT_NAME"),
|
|
@@ -265,18 +364,10 @@ pub fn init(ruby: &Ruby) {
|
|
|
265
364
|
http2: frozen(ruby, "HTTP/2"),
|
|
266
365
|
methods,
|
|
267
366
|
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
|
-
)),
|
|
367
|
+
slab,
|
|
368
|
+
hosts: lru(HOST_CACHE_CAP),
|
|
369
|
+
addrs: lru(ADDR_CACHE_CAP),
|
|
370
|
+
values: lru(VALUE_CACHE_CAP),
|
|
280
371
|
internable: INTERNABLE_VALUES.iter().copied().collect(),
|
|
281
372
|
errors_stream: RwLock::new(None),
|
|
282
373
|
null_input: RwLock::new(None),
|
|
@@ -288,18 +379,24 @@ pub fn get() -> &'static EnvStrings {
|
|
|
288
379
|
ENV_STRINGS.get().expect("env_strings::init not called")
|
|
289
380
|
}
|
|
290
381
|
|
|
291
|
-
/// Called once from lib/kino.rb (main ractor) with the
|
|
292
|
-
///
|
|
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).
|
|
293
388
|
pub fn register_defaults(
|
|
294
389
|
ruby: &Ruby,
|
|
295
390
|
errors: Value,
|
|
296
391
|
null_input: Value,
|
|
297
392
|
) -> Result<(), magnus::Error> {
|
|
393
|
+
let ractor: Value = ruby.class_object().const_get("Ractor")?;
|
|
298
394
|
for value in [errors, null_input] {
|
|
299
|
-
|
|
395
|
+
let shareable: bool = ractor.funcall("shareable?", (value,))?;
|
|
396
|
+
if !shareable {
|
|
300
397
|
return Err(magnus::Error::new(
|
|
301
398
|
ruby.exception_arg_error(),
|
|
302
|
-
"register_defaults expects
|
|
399
|
+
"register_defaults expects Ractor-shareable objects",
|
|
303
400
|
));
|
|
304
401
|
}
|
|
305
402
|
}
|
|
@@ -311,9 +408,41 @@ pub fn register_defaults(
|
|
|
311
408
|
Ok(())
|
|
312
409
|
}
|
|
313
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
|
+
|
|
314
444
|
/// 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.
|
|
445
|
+
/// (and caching) frozen values on miss.
|
|
317
446
|
pub fn set_host_env(
|
|
318
447
|
ruby: &Ruby,
|
|
319
448
|
env: magnus::RHash,
|
|
@@ -321,21 +450,17 @@ pub fn set_host_env(
|
|
|
321
450
|
make: impl FnOnce() -> (String, u16),
|
|
322
451
|
) -> Result<(), magnus::Error> {
|
|
323
452
|
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
|
-
};
|
|
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
|
+
);
|
|
339
464
|
env.aset(ruby.get_inner(s.server_name), name)?;
|
|
340
465
|
env.aset(ruby.get_inner(s.server_port), port)?;
|
|
341
466
|
Ok(())
|
|
@@ -354,32 +479,44 @@ pub fn set_authority_env(
|
|
|
354
479
|
make: impl FnOnce() -> (String, u16),
|
|
355
480
|
) -> Result<(), magnus::Error> {
|
|
356
481
|
let s = get();
|
|
357
|
-
let
|
|
358
|
-
let
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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),
|
|
362
499
|
}
|
|
363
|
-
(
|
|
364
|
-
entry.name.get(),
|
|
365
|
-
entry.port.get(),
|
|
366
|
-
entry.host.as_ref().expect("just filled").get(),
|
|
367
|
-
)
|
|
368
500
|
}
|
|
369
501
|
None => {
|
|
370
|
-
let (
|
|
371
|
-
let
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
entry
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
+
}
|
|
383
520
|
}
|
|
384
521
|
};
|
|
385
522
|
env.aset(ruby.get_inner(s.server_name), name)?;
|
|
@@ -391,8 +528,7 @@ pub fn set_authority_env(
|
|
|
391
528
|
|
|
392
529
|
/// Set one header's value on `env` under `key`: through the interned
|
|
393
530
|
/// 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.
|
|
531
|
+
/// length), else a fresh per-request string.
|
|
396
532
|
pub fn set_value_env(
|
|
397
533
|
ruby: &Ruby,
|
|
398
534
|
env: magnus::RHash,
|
|
@@ -404,36 +540,29 @@ pub fn set_value_env(
|
|
|
404
540
|
if value.len() > VALUE_INTERN_MAX_LEN || !s.internable.contains(name) {
|
|
405
541
|
return env.aset(key, ruby.str_from_slice(value));
|
|
406
542
|
}
|
|
407
|
-
let
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
};
|
|
417
|
-
env.aset(key, cached)?;
|
|
418
|
-
Ok(())
|
|
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)
|
|
419
552
|
}
|
|
420
553
|
|
|
421
|
-
/// Set REMOTE_ADDR on `env` from the LRU peer-IP cache
|
|
422
|
-
/// contract as set_host_env.
|
|
554
|
+
/// Set REMOTE_ADDR on `env` from the LRU peer-IP cache.
|
|
423
555
|
pub fn set_addr_env(ruby: &Ruby, env: magnus::RHash, ip: IpAddr) -> Result<(), magnus::Error> {
|
|
424
556
|
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(())
|
|
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)
|
|
437
566
|
}
|
|
438
567
|
|
|
439
568
|
#[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
|
}
|
data/ext/kino/src/pin.rs
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
//! GVL held), release is a single atomic store from whatever tokio
|
|
23
23
|
//! thread drops the buffer, and the mark hook only loads atomics, so
|
|
24
24
|
//! no path takes a lock. A full slab degrades to the copy path.
|
|
25
|
+
//!
|
|
26
|
+
//! The slab is also how env_strings.rs roots its cached env strings (a
|
|
27
|
+
//! second slab, marked through its own keeper), for the same reason: no
|
|
28
|
+
//! GC registration API may be called from parallel ractors.
|
|
25
29
|
|
|
26
30
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
|
27
31
|
use std::sync::Arc;
|
|
@@ -50,8 +54,9 @@ extern "C" {
|
|
|
50
54
|
fn rb_str_tmp_frozen_acquire(orig: rb_sys::VALUE) -> rb_sys::VALUE;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
///
|
|
54
|
-
/// VALUE of 0 is Qfalse, which can
|
|
57
|
+
/// A fixed slab of atomic VALUE slots, each a pinning GC root while it
|
|
58
|
+
/// is non-zero. Slot value 0 = empty (a VALUE of 0 is Qfalse, which can
|
|
59
|
+
/// never be a rooted string).
|
|
55
60
|
pub struct PinSlab {
|
|
56
61
|
slots: Box<[AtomicU64]>,
|
|
57
62
|
/// Rotating claim cursor: keeps the free-slot scan O(1) amortized.
|
|
@@ -59,15 +64,21 @@ pub struct PinSlab {
|
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
impl PinSlab {
|
|
67
|
+
/// A slab for one server's in-flight response buffers.
|
|
62
68
|
pub fn new() -> Self {
|
|
69
|
+
Self::with_capacity(SLAB_CAPACITY)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pub fn with_capacity(capacity: usize) -> Self {
|
|
63
73
|
PinSlab {
|
|
64
|
-
slots: (0..
|
|
74
|
+
slots: (0..capacity).map(|_| AtomicU64::new(0)).collect(),
|
|
65
75
|
cursor: AtomicUsize::new(0),
|
|
66
76
|
}
|
|
67
77
|
}
|
|
68
78
|
|
|
69
|
-
/// Root `value`; None when the slab is full (caller
|
|
70
|
-
|
|
79
|
+
/// Root `value`; None when the slab is full (the caller falls back to
|
|
80
|
+
/// a copy, or to an uncached string).
|
|
81
|
+
pub(crate) fn insert(&self, value: rb_sys::VALUE) -> Option<usize> {
|
|
71
82
|
let start = self.cursor.fetch_add(1, Ordering::Relaxed);
|
|
72
83
|
for offset in 0..self.slots.len() {
|
|
73
84
|
let index = (start + offset) % self.slots.len();
|
|
@@ -83,7 +94,7 @@ impl PinSlab {
|
|
|
83
94
|
|
|
84
95
|
/// Drop the root. Called from tokio threads: a plain atomic store,
|
|
85
96
|
/// no Ruby API. The string stays alive until the next GC sweep.
|
|
86
|
-
fn release(&self, index: usize) {
|
|
97
|
+
pub(crate) fn release(&self, index: usize) {
|
|
87
98
|
self.slots[index].store(0, Ordering::SeqCst);
|
|
88
99
|
}
|
|
89
100
|
|
|
@@ -113,9 +124,10 @@ impl PinSlab {
|
|
|
113
124
|
}
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
/// The GC-visible face of a
|
|
117
|
-
///
|
|
118
|
-
///
|
|
127
|
+
/// The GC-visible face of a PinSlab: Ruby's GC marks every rooted string
|
|
128
|
+
/// through it. `Kino::Server` holds one for its response-buffer slab for
|
|
129
|
+
/// the server's lifetime (surviving worker-ractor crashes); env_strings
|
|
130
|
+
/// registers one for its cache slab as an immortal root at init.
|
|
119
131
|
#[derive(magnus::TypedData)]
|
|
120
132
|
#[magnus(class = "Kino::Native::PinKeeper", free_immediately, mark)]
|
|
121
133
|
pub struct PinKeeper(pub Arc<PinSlab>);
|