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.
@@ -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 them immortal. Built
12
- //! once, with the GVL held, on the main ractor; read-only afterwards.
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::{gc, prelude::*, RString, Ruby, Value};
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: entries are BoxValue-rooted (registered with the GC on
56
- /// insert, UNregistered on eviction-drop), so a rotating-host attack
57
- /// recycles cache slots instead of leaking immortal strings.
58
- pub hosts: Mutex<lru::LruCache<Vec<u8>, HostEntry, ahash::RandomState>>,
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<lru::LruCache<Vec<u8>, CachedStr, ahash::RandomState>>,
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
- /// A frozen RString rooted via BoxValue (GC-registered address; unregisters
124
- /// on Drop, so LRU eviction actually frees the string).
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
- /// SAFETY of Send + Sync: the contents are frozen Ruby strings (therefore
127
- /// Ractor-shareable), and creation, reads, and drops all happen while the
128
- /// calling thread holds its GVL (native method context) AND the owning
129
- /// cache Mutex; readers root the value into a live env Hash before
130
- /// releasing the lock, so an eviction on another ractor can never free a
131
- /// string that a reader still holds unrooted.
132
- pub struct CachedStr(magnus::value::BoxValue<RString>);
133
- unsafe impl Send for CachedStr {}
134
- unsafe impl Sync for CachedStr {}
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
- fn new(ruby: &Ruby, s: &str) -> Self {
138
- let string = ruby.str_new(s);
139
- string.freeze();
140
- CachedStr(magnus::value::BoxValue::new(string))
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
- /// Header values are bytes on the wire (not guaranteed UTF-8), so
144
- /// they cache as the same binary strings `str_from_slice` builds on
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
- fn get(&self) -> RString {
154
- *self.0
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.str_new(s);
229
- string.freeze();
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
- hosts: Mutex::new(lru::LruCache::with_hasher(
269
- std::num::NonZeroUsize::new(HOST_CACHE_CAP).unwrap(),
270
- ahash::RandomState::new(),
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 frozen,
292
- /// Ractor-shareable singletons the Ruby layer owns.
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
- if !value.is_frozen() {
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 frozen objects",
413
+ "register_defaults expects Ractor-shareable objects",
303
414
  ));
304
415
  }
305
416
  }
306
- gc::register_mark_object(errors);
307
- gc::register_mark_object(null_input);
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. The aset happens UNDER the cache
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 mut hosts = s.hosts.lock();
325
- let (name, port) = match hosts.get(host) {
326
- Some(entry) => (entry.name.get(), entry.port.get()),
327
- None => {
328
- let (name_s, port_n) = make();
329
- let entry = HostEntry {
330
- name: CachedStr::new(ruby, &name_s),
331
- port: CachedStr::new(ruby, &port_n.to_string()),
332
- host: None,
333
- };
334
- let values = (entry.name.get(), entry.port.get());
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 mut hosts = s.hosts.lock();
358
- let (name, port, host) = match hosts.get_mut(authority.as_bytes()) {
359
- Some(entry) => {
360
- if entry.host.is_none() {
361
- entry.host = Some(CachedStr::new(ruby, authority));
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 (name_s, port_n) = make();
371
- let entry = HostEntry {
372
- name: CachedStr::new(ruby, &name_s),
373
- port: CachedStr::new(ruby, &port_n.to_string()),
374
- host: Some(CachedStr::new(ruby, authority)),
375
- };
376
- let values = (
377
- entry.name.get(),
378
- entry.port.get(),
379
- entry.host.as_ref().expect("just built").get(),
380
- );
381
- hosts.put(authority.as_bytes().to_vec(), entry);
382
- values
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. The cached aset happens
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 mut values = s.values.lock();
408
- let cached = match values.get(value) {
409
- Some(cached) => cached.get(),
410
- None => {
411
- let entry = CachedStr::new_from_slice(ruby, value);
412
- let string = entry.get();
413
- values.put(value.to_vec(), entry); // may evict + free an old value
414
- string
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; same locking
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 mut addrs = s.addrs.lock();
426
- let value = match addrs.get(&ip) {
427
- Some(cached) => cached.get(),
428
- None => {
429
- let entry = CachedStr::new(ruby, &ip.to_string());
430
- let value = entry.get();
431
- addrs.put(ip, entry);
432
- value
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
  }