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 +4 -4
- data/CHANGELOG.md +12 -0
- data/ext/maxmind_db_rust/Cargo.toml +2 -2
- data/ext/maxmind_db_rust/src/lib.rs +101 -67
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 854691c99c81b7d9574c780a7e10f80eea69788bb4dc95cce41361e5163d6f28
|
|
4
|
+
data.tar.gz: '086f7efa6e3620e3fd15d66b87964076647e7510551d58d98e4f484c69400173'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
15
|
+
arc-swap = "1.9"
|
|
16
16
|
ipnetwork = "0.21"
|
|
17
17
|
magnus = "0.8"
|
|
18
|
-
maxminddb = { version = "0.
|
|
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::
|
|
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
|
-
|
|
36
|
-
|
|
47
|
+
struct StringCache {
|
|
48
|
+
entries: Box<[StringCacheEntry]>,
|
|
37
49
|
}
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
57
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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) =>
|
|
307
|
-
|
|
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
|
|
1008
|
-
|
|
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.
|
|
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.
|
|
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: []
|