maxmind-db-rust 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c9ce85a0f2ba6a7852b6b3b12ffacc517f2a484f41fb686b131df4e7c7685fb
4
- data.tar.gz: 85f03d7a87b5b855cdbe66e00106503692d9ef13270f9f99a2074210f042291a
3
+ metadata.gz: 854691c99c81b7d9574c780a7e10f80eea69788bb4dc95cce41361e5163d6f28
4
+ data.tar.gz: '086f7efa6e3620e3fd15d66b87964076647e7510551d58d98e4f484c69400173'
5
5
  SHA512:
6
- metadata.gz: adadcd0e268b04c7aafa8d9540c62dce31158388c03e4539b9b579890d9572606cba2e83275e7d5f511e7c885c9b9ee0015ed581cc194b93618a5a8b89f1437c
7
- data.tar.gz: b6c8eb6ddba493e62d145d2c028db95c13a54b8a64e41aff4d7651604890d77c31aac29f3eb5370c85da46e777369fc3c281d574408d5c0d965cfef3217c3159
6
+ metadata.gz: f20905fca81e3742ff75da2e5758fbd198d61dd5c7c82b23dd8ed9a731bf7a46dc707c6660590adb895e2c022b6f6d07edb7916eab774d4461faccd2de66ec2e
7
+ data.tar.gz: 888d4623247bf07a0c8058c669230032ebcef98b0efcc03920f1196cb1695361f2a868250ffeafb6fe3ed7913038b38f4b419d9ef5e3a1d5f3caecd0b1ec1bb0
data/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-04-25
9
+
10
+ ### Performance
11
+
12
+ - Restored lookup performance with a generic bounded cache of frozen Ruby strings reused across decoded keys and scalar values.
13
+ - Removed hardcoded interned string tables in favor of the generic string cache.
14
+ - Simplified decoding so lookups and iteration use the same `maxminddb` decode path again.
15
+ - Reduced repeated cache-root lookup overhead with a thread-local `OnceCell` for the Ruby-owned string cache roots.
16
+ - Borrowed decoded map keys directly during deserialization to avoid `Cow` overhead in the hot decode path.
17
+ - Upgraded `maxminddb` crate to 0.28.0, which includes several performance
18
+ improvements.
19
+
8
20
  ## [0.3.0] - 2026-02-22
9
21
 
10
22
  ### Changed
@@ -12,10 +12,10 @@ name = "maxmind_db_rust"
12
12
  crate-type = ["cdylib"]
13
13
 
14
14
  [dependencies]
15
- arc-swap = "1.8"
15
+ arc-swap = "1.9"
16
16
  ipnetwork = "0.21"
17
17
  magnus = "0.8"
18
- maxminddb = { version = "0.27", features = ["unsafe-str-decode"] }
18
+ maxminddb = { version = "0.28", features = ["unsafe-str-decode"] }
19
19
  memmap2 = "0.9"
20
20
  rustc-hash = "2.1"
21
21
  serde = "1.0"
@@ -10,13 +10,14 @@ use magnus::{
10
10
  };
11
11
  use maxminddb_crate::{MaxMindDbError, Reader as MaxMindReader, Within};
12
12
  use memmap2::Mmap;
13
- use rustc_hash::FxHashMap;
13
+ use rustc_hash::FxHasher;
14
14
  use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
15
15
  use std::{
16
- cell::RefCell,
16
+ cell::{OnceCell, RefCell},
17
17
  collections::BTreeMap,
18
18
  fmt,
19
19
  fs::File,
20
+ hash::{Hash, Hasher},
20
21
  io::Read as IoRead,
21
22
  net::IpAddr,
22
23
  path::Path,
@@ -31,57 +32,94 @@ use std::{
31
32
  const ERR_CLOSED_DB: &str = "Attempt to read from a closed MaxMind DB.";
32
33
  const ERR_BAD_DATA: &str =
33
34
  "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)";
35
+ const STRING_CACHE_ROOTS_CONST: &str = "__STRING_CACHE_ROOTS__";
36
+ const MAP_KEY_ROOTS_CONST: &str = "__MAP_KEY_ROOTS__";
37
+ const STRING_CACHE_MAX: usize = 4096;
38
+ const STRING_CACHE_MIN_LEN: usize = 2;
39
+ const STRING_CACHE_MAX_LEN: usize = 64;
40
+
41
+ #[derive(Default)]
42
+ struct StringCacheEntry {
43
+ hash: u64,
44
+ value: String,
45
+ }
34
46
 
35
- thread_local! {
36
- static MAP_KEY_CACHE: RefCell<MapKeyCache> = RefCell::new(MapKeyCache::new());
47
+ struct StringCache {
48
+ entries: Box<[StringCacheEntry]>,
37
49
  }
38
50
 
39
- const MAP_KEY_CACHE_MAX: usize = 256;
40
- const MAP_KEY_ROOTS_CONST: &str = "__MAP_KEY_ROOTS__";
51
+ impl StringCache {
52
+ fn new() -> Self {
53
+ let entries = (0..STRING_CACHE_MAX)
54
+ .map(|_| StringCacheEntry::default())
55
+ .collect::<Vec<_>>()
56
+ .into_boxed_slice();
57
+ Self { entries }
58
+ }
59
+ }
41
60
 
42
- struct MapKeyCache {
43
- key_to_index: FxHashMap<String, usize>,
61
+ thread_local! {
62
+ static STRING_CACHE: RefCell<StringCache> = RefCell::new(StringCache::new());
63
+ static STRING_CACHE_ROOTS: OnceCell<RArray> = const { OnceCell::new() };
44
64
  }
45
65
 
46
- impl MapKeyCache {
47
- #[inline]
48
- fn new() -> Self {
49
- Self {
50
- key_to_index: FxHashMap::default(),
51
- }
66
+ #[inline]
67
+ fn string_cache_roots_owner(ruby: &magnus::Ruby) -> RArray {
68
+ let value = rust_module(ruby)
69
+ .const_get::<_, Value>(STRING_CACHE_ROOTS_CONST)
70
+ .expect("string cache roots constant should exist");
71
+ RArray::from_value(value).expect("string cache roots constant should be an array")
72
+ }
73
+
74
+ #[inline]
75
+ fn init_thread_string_cache_roots(ruby: &magnus::Ruby) -> RArray {
76
+ let roots = ruby.ary_new_capa(STRING_CACHE_MAX);
77
+ for _ in 0..STRING_CACHE_MAX {
78
+ roots
79
+ .push(ruby.qnil().as_value())
80
+ .expect("string cache roots initialization should succeed");
52
81
  }
82
+ string_cache_roots_owner(ruby)
83
+ .push(roots.as_value())
84
+ .expect("string cache roots owner should retain per-thread roots");
85
+ roots
53
86
  }
54
87
 
55
88
  #[inline]
56
- fn map_key_roots_array(ruby: &magnus::Ruby) -> RArray {
57
- let rust = rust_module(ruby);
58
- let roots = rust
59
- .const_get::<_, Value>(MAP_KEY_ROOTS_CONST)
60
- .expect("map key roots constant should exist");
61
- RArray::from_value(roots).expect("map key roots constant should be an array")
89
+ fn string_cache_roots(ruby: &magnus::Ruby) -> RArray {
90
+ STRING_CACHE_ROOTS.with(|roots| *roots.get_or_init(|| init_thread_string_cache_roots(ruby)))
62
91
  }
63
92
 
64
93
  #[inline]
65
- fn cached_map_key(ruby: &magnus::Ruby, key: &str) -> Value {
66
- MAP_KEY_CACHE.with(|cache| {
67
- let mut cache = cache.borrow_mut();
68
- let roots = map_key_roots_array(ruby);
69
- if let Some(index) = cache.key_to_index.get(key) {
70
- return roots
71
- .entry::<Value>(*index as isize)
72
- .expect("cached map key index should be valid");
73
- }
94
+ fn cached_string(ruby: &magnus::Ruby, value: &str) -> Value {
95
+ if !(STRING_CACHE_MIN_LEN..=STRING_CACHE_MAX_LEN).contains(&value.len()) {
96
+ return ruby.str_new(value).into_value_with(ruby);
97
+ }
74
98
 
75
- let interned = ruby.str_new(key).to_interned_str();
76
- let value = interned.as_value();
77
- if cache.key_to_index.len() < MAP_KEY_CACHE_MAX {
78
- let index = roots.len();
79
- roots
80
- .push(value)
81
- .expect("map key roots array push should succeed");
82
- cache.key_to_index.insert(key.to_owned(), index);
99
+ let mut hasher = FxHasher::default();
100
+ value.hash(&mut hasher);
101
+ let hash = hasher.finish();
102
+ let slot = (hash as usize) & (STRING_CACHE_MAX - 1);
103
+
104
+ STRING_CACHE.with(|cache_cell| {
105
+ let mut cache = cache_cell.borrow_mut();
106
+ let entry = &mut cache.entries[slot];
107
+ if entry.hash == hash && entry.value == value {
108
+ return string_cache_roots(ruby)
109
+ .entry::<Value>(slot as isize)
110
+ .expect("string cache roots lookup should succeed");
83
111
  }
84
- value
112
+
113
+ let string = ruby.str_new(value);
114
+ string.freeze();
115
+ let cached = string.as_value();
116
+ string_cache_roots(ruby)
117
+ .store(slot as isize, cached)
118
+ .expect("string cache roots update should succeed");
119
+ entry.hash = hash;
120
+ entry.value.clear();
121
+ entry.value.push_str(value);
122
+ cached
85
123
  })
86
124
  }
87
125
 
@@ -214,18 +252,14 @@ impl<'de, 'ruby> Visitor<'de> for RubyValueVisitor<'ruby> {
214
252
  where
215
253
  E: de::Error,
216
254
  {
217
- Ok(RubyDecodedValue::new(
218
- self.ruby.str_new(value).into_value_with(self.ruby),
219
- ))
255
+ Ok(RubyDecodedValue::new(cached_string(self.ruby, value)))
220
256
  }
221
257
 
222
258
  fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
223
259
  where
224
260
  E: de::Error,
225
261
  {
226
- Ok(RubyDecodedValue::new(
227
- self.ruby.str_new(&value).into_value_with(self.ruby),
228
- ))
262
+ Ok(RubyDecodedValue::new(cached_string(self.ruby, &value)))
229
263
  }
230
264
 
231
265
  fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
@@ -270,8 +304,8 @@ impl<'de, 'ruby> Visitor<'de> for RubyValueVisitor<'ruby> {
270
304
  None => self.ruby.hash_new(),
271
305
  };
272
306
  while let Some(key) = map.next_key::<&'de str>()? {
273
- let key_val = cached_map_key(self.ruby, key);
274
307
  let value = map.next_value_seed(RubyValueSeed { ruby: self.ruby })?;
308
+ let key_val = cached_string(self.ruby, key);
275
309
  hash.aset(key_val, value.into_value())
276
310
  .map_err(|e| de::Error::custom(e.to_string()))?;
277
311
  }
@@ -302,10 +336,19 @@ impl ReaderSource {
302
336
  &self,
303
337
  ip: IpAddr,
304
338
  ) -> Result<(Option<RubyDecodedValue>, usize), maxminddb_crate::MaxMindDbError> {
305
- match self {
306
- ReaderSource::Mmap(reader) => lookup_prefix_for_reader(reader, ip),
307
- ReaderSource::Memory(reader) => lookup_prefix_for_reader(reader, ip),
308
- }
339
+ let (result, prefix_len) = match self {
340
+ ReaderSource::Mmap(reader) => {
341
+ let result = reader.lookup(ip)?;
342
+ let network = result.network()?;
343
+ (result.decode()?, prefix_len_for_ip_network(ip, network))
344
+ }
345
+ ReaderSource::Memory(reader) => {
346
+ let result = reader.lookup(ip)?;
347
+ let network = result.network()?;
348
+ (result.decode()?, prefix_len_for_ip_network(ip, network))
349
+ }
350
+ };
351
+ Ok((result, prefix_len))
309
352
  }
310
353
 
311
354
  #[inline]
@@ -321,15 +364,12 @@ impl ReaderSource {
321
364
  match self {
322
365
  ReaderSource::Mmap(reader) => {
323
366
  let iter = reader.within(network, Default::default())?;
324
- // SAFETY: the iterator holds a reference into `reader`. We'll store an Arc guard
325
- // alongside it so the reader outlives the transmuted iterator.
326
367
  Ok(ReaderWithin::Mmap(unsafe {
327
368
  std::mem::transmute::<Within<'_, Mmap>, Within<'static, Mmap>>(iter)
328
369
  }))
329
370
  }
330
371
  ReaderSource::Memory(reader) => {
331
372
  let iter = reader.within(network, Default::default())?;
332
- // SAFETY: same as above, the Arc guard keeps the reader alive.
333
373
  Ok(ReaderWithin::Memory(unsafe {
334
374
  std::mem::transmute::<Within<'_, Vec<u8>>, Within<'static, Vec<u8>>>(iter)
335
375
  }))
@@ -353,17 +393,6 @@ impl ReaderWithin {
353
393
  }
354
394
  }
355
395
 
356
- #[inline]
357
- fn lookup_prefix_for_reader<S: AsRef<[u8]>>(
358
- reader: &MaxMindReader<S>,
359
- ip: IpAddr,
360
- ) -> Result<(Option<RubyDecodedValue>, usize), maxminddb_crate::MaxMindDbError> {
361
- let result = reader.lookup(ip)?;
362
- let network = result.network()?;
363
- let prefix_len = prefix_len_for_ip_network(ip, network);
364
- Ok((result.decode()?, prefix_len))
365
- }
366
-
367
396
  #[inline]
368
397
  // prefix_len_for_ip_network uses 0 as a sentinel for ip.is_ipv4() && network.is_ipv6().
369
398
  // In this case, 0 is not a real prefix length; it signals an IPv4-in-IPv6 mapping path,
@@ -725,7 +754,6 @@ impl Reader {
725
754
  format!("Failed to iterate: {}", e),
726
755
  )
727
756
  })?;
728
-
729
757
  // Get IPAddr class
730
758
  let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
731
759
 
@@ -869,7 +897,6 @@ fn open_database_mmap(path: &str) -> Result<Reader, Error> {
869
897
  format!("Failed to memory-map database file: {}", e),
870
898
  )
871
899
  })?;
872
-
873
900
  let reader = MaxMindReader::from_source(mmap).map_err(|_| {
874
901
  Error::new(
875
902
  ExceptionClass::from_value(invalid_database_error().as_value())
@@ -1004,8 +1031,15 @@ fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
1004
1031
  }
1005
1032
  };
1006
1033
 
1007
- if rust.const_get::<_, Value>(MAP_KEY_ROOTS_CONST).is_err() {
1008
- rust.const_set(MAP_KEY_ROOTS_CONST, ruby.ary_new_capa(MAP_KEY_CACHE_MAX))?;
1034
+ if rust
1035
+ .const_get::<_, Value>(STRING_CACHE_ROOTS_CONST)
1036
+ .is_err()
1037
+ {
1038
+ rust.const_set(STRING_CACHE_ROOTS_CONST, ruby.ary_new())?;
1039
+ }
1040
+
1041
+ if rust.const_get::<_, Value>(MAP_KEY_ROOTS_CONST).is_ok() {
1042
+ let _ = rust.funcall::<_, _, Value>("send", ("remove_const", MAP_KEY_ROOTS_CONST))?;
1009
1043
  }
1010
1044
 
1011
1045
  // The extension can be loaded more than once from different paths.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maxmind-db-rust
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregory Oschwald
@@ -191,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
191
191
  - !ruby/object:Gem::Version
192
192
  version: '0'
193
193
  requirements: []
194
- rubygems_version: 4.0.3
194
+ rubygems_version: 4.0.6
195
195
  specification_version: 4
196
196
  summary: Unofficial high-performance Rust-based MaxMind DB reader for Ruby
197
197
  test_files: []