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.
@@ -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 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
+ //! 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
- /// Host-header bytes -> frozen (SERVER_NAME, SERVER_PORT) values, and
51
- /// peer IP -> frozen REMOTE_ADDR value. Real traffic has low
52
- /// cardinality on both, so these kill 3 string allocations per request.
53
- /// LRU-bounded: entries are BoxValue-rooted (registered with the GC on
54
- /// insert, UNregistered on eviction-drop), so a rotating-host attack
55
- /// recycles cache slots instead of leaking immortal strings.
56
- pub hosts: Mutex<lru::LruCache<Vec<u8>, (CachedStr, CachedStr), ahash::RandomState>>,
57
- pub addrs: Mutex<lru::LruCache<IpAddr, CachedStr, ahash::RandomState>>,
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
- /// A frozen RString rooted via BoxValue (GC-registered address; unregisters
68
- /// on Drop, so LRU eviction actually frees the string).
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
- /// SAFETY of Send + Sync: the contents are frozen Ruby strings (therefore
71
- /// Ractor-shareable), and creation, reads, and drops all happen while the
72
- /// calling thread holds its GVL (native method context) AND the owning
73
- /// cache Mutex; readers root the value into a live env Hash before
74
- /// releasing the lock, so an eviction on another ractor can never free a
75
- /// string that a reader still holds unrooted.
76
- pub struct CachedStr(magnus::value::BoxValue<RString>);
77
- unsafe impl Send for CachedStr {}
78
- unsafe impl Sync for CachedStr {}
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
- fn new(ruby: &Ruby, s: &str) -> Self {
82
- let string = ruby.str_new(s);
83
- string.freeze();
84
- CachedStr(magnus::value::BoxValue::new(string))
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
- *self.0
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.str_new(s);
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
- hosts: Mutex::new(lru::LruCache::with_hasher(
200
- std::num::NonZeroUsize::new(HOST_CACHE_CAP).unwrap(),
201
- ahash::RandomState::new(),
202
- )),
203
- addrs: Mutex::new(lru::LruCache::with_hasher(
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 frozen,
218
- /// Ractor-shareable singletons the Ruby layer owns.
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
- if !value.is_frozen() {
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 frozen objects",
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. The aset happens UNDER the cache
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 mut hosts = s.hosts.lock();
251
- let (name, port) = match hosts.get(host) {
252
- Some((name, port)) => (name.get(), port.get()),
253
- None => {
254
- let (name_s, port_n) = make();
255
- let entry = (
256
- CachedStr::new(ruby, &name_s),
257
- CachedStr::new(ruby, &port_n.to_string()),
258
- );
259
- let values = (entry.0.get(), entry.1.get());
260
- hosts.put(host.to_vec(), entry); // may evict + free an old pair
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 REMOTE_ADDR on `env` from the LRU peer-IP cache; same locking
270
- /// contract as set_host_env.
271
- pub fn set_addr_env(ruby: &Ruby, env: magnus::RHash, ip: IpAddr) -> Result<(), magnus::Error> {
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 mut addrs = s.addrs.lock();
274
- let value = match addrs.get(&ip) {
275
- Some(cached) => cached.get(),
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 entry = CachedStr::new(ruby, &ip.to_string());
278
- let value = entry.get();
279
- addrs.put(ip, entry);
280
- value
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.remote_addr), value)?;
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("take_one", function!(queue::take_one, 2))?;
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, 6))?;
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, 5),
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 cache on the main ractor: magnus
102
- // resolves it lazily on first wrap, and a racy first resolution from two
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
  }