vinted-prometheus-client-mmap 1.5.0-x86_64-linux
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 +7 -0
- data/README.md +5 -0
- data/ext/fast_mmaped_file_rs/Cargo.toml +40 -0
- data/ext/fast_mmaped_file_rs/README.md +52 -0
- data/ext/fast_mmaped_file_rs/build.rs +7 -0
- data/ext/fast_mmaped_file_rs/extconf.rb +28 -0
- data/ext/fast_mmaped_file_rs/src/error.rs +174 -0
- data/ext/fast_mmaped_file_rs/src/exemplars.rs +25 -0
- data/ext/fast_mmaped_file_rs/src/file_entry.rs +1252 -0
- data/ext/fast_mmaped_file_rs/src/file_info.rs +240 -0
- data/ext/fast_mmaped_file_rs/src/lib.rs +89 -0
- data/ext/fast_mmaped_file_rs/src/macros.rs +14 -0
- data/ext/fast_mmaped_file_rs/src/map.rs +519 -0
- data/ext/fast_mmaped_file_rs/src/metrics.proto +153 -0
- data/ext/fast_mmaped_file_rs/src/mmap/inner.rs +775 -0
- data/ext/fast_mmaped_file_rs/src/mmap.rs +977 -0
- data/ext/fast_mmaped_file_rs/src/raw_entry.rs +547 -0
- data/ext/fast_mmaped_file_rs/src/testhelper.rs +222 -0
- data/ext/fast_mmaped_file_rs/src/util.rs +140 -0
- data/lib/.DS_Store +0 -0
- data/lib/2.7/fast_mmaped_file_rs.so +0 -0
- data/lib/3.0/fast_mmaped_file_rs.so +0 -0
- data/lib/3.1/fast_mmaped_file_rs.so +0 -0
- data/lib/3.2/fast_mmaped_file_rs.so +0 -0
- data/lib/3.3/fast_mmaped_file_rs.so +0 -0
- data/lib/prometheus/.DS_Store +0 -0
- data/lib/prometheus/client/configuration.rb +24 -0
- data/lib/prometheus/client/counter.rb +27 -0
- data/lib/prometheus/client/formats/protobuf.rb +93 -0
- data/lib/prometheus/client/formats/text.rb +85 -0
- data/lib/prometheus/client/gauge.rb +40 -0
- data/lib/prometheus/client/helper/entry_parser.rb +132 -0
- data/lib/prometheus/client/helper/file_locker.rb +50 -0
- data/lib/prometheus/client/helper/json_parser.rb +23 -0
- data/lib/prometheus/client/helper/metrics_processing.rb +45 -0
- data/lib/prometheus/client/helper/metrics_representation.rb +51 -0
- data/lib/prometheus/client/helper/mmaped_file.rb +64 -0
- data/lib/prometheus/client/helper/plain_file.rb +29 -0
- data/lib/prometheus/client/histogram.rb +80 -0
- data/lib/prometheus/client/label_set_validator.rb +85 -0
- data/lib/prometheus/client/metric.rb +80 -0
- data/lib/prometheus/client/mmaped_dict.rb +83 -0
- data/lib/prometheus/client/mmaped_value.rb +164 -0
- data/lib/prometheus/client/page_size.rb +17 -0
- data/lib/prometheus/client/push.rb +203 -0
- data/lib/prometheus/client/rack/collector.rb +88 -0
- data/lib/prometheus/client/rack/exporter.rb +102 -0
- data/lib/prometheus/client/registry.rb +65 -0
- data/lib/prometheus/client/simple_value.rb +31 -0
- data/lib/prometheus/client/summary.rb +69 -0
- data/lib/prometheus/client/support/puma.rb +44 -0
- data/lib/prometheus/client/support/unicorn.rb +35 -0
- data/lib/prometheus/client/uses_value_type.rb +20 -0
- data/lib/prometheus/client/version.rb +5 -0
- data/lib/prometheus/client.rb +58 -0
- data/lib/prometheus.rb +3 -0
- metadata +210 -0
@@ -0,0 +1,775 @@
|
|
1
|
+
use libc::off_t;
|
2
|
+
use memmap2::{MmapMut, MmapOptions};
|
3
|
+
use nix::libc::c_long;
|
4
|
+
use std::fs::File;
|
5
|
+
use std::mem::size_of;
|
6
|
+
use std::ops::Range;
|
7
|
+
use std::os::unix::prelude::{AsRawFd, RawFd};
|
8
|
+
use std::path::PathBuf;
|
9
|
+
|
10
|
+
use crate::error::{MmapError, RubyError};
|
11
|
+
use crate::raw_entry::RawEntry;
|
12
|
+
use crate::exemplars::{Exemplar, EXEMPLAR_ENTRY_MAX_SIZE_BYTES};
|
13
|
+
|
14
|
+
use crate::util::{read_exemplar, CheckedOps};
|
15
|
+
use crate::util::{self, errno, read_f64, read_u32};
|
16
|
+
use crate::Result;
|
17
|
+
use crate::HEADER_SIZE;
|
18
|
+
use std::iter;
|
19
|
+
|
20
|
+
/// A mmapped file and its metadata. Ruby never directly interfaces
|
21
|
+
/// with this struct.
|
22
|
+
#[derive(Debug)]
|
23
|
+
pub(super) struct InnerMmap {
|
24
|
+
/// The handle of the file being mmapped. When resizing the
|
25
|
+
/// file we must drop the `InnerMmap` while keeping this open,
|
26
|
+
/// truncate/extend the file, and establish a new `InnerMmap` to
|
27
|
+
/// re-map it.
|
28
|
+
file: File,
|
29
|
+
/// The path of the file.
|
30
|
+
path: PathBuf,
|
31
|
+
/// The mmap itself. When initializing a new entry the length of
|
32
|
+
/// the mmap is used for bounds checking.
|
33
|
+
map: MmapMut,
|
34
|
+
/// The length of data written to the file, used to validate
|
35
|
+
/// whether a `load/save_value` call is in bounds and the length
|
36
|
+
/// we truncate the file to when unmapping.
|
37
|
+
///
|
38
|
+
/// Equivalent to `i_mm->t->real` in the C implementation.
|
39
|
+
len: usize,
|
40
|
+
}
|
41
|
+
|
42
|
+
impl InnerMmap {
|
43
|
+
/// Constructs a new `InnerMmap`, mmapping `path`.
|
44
|
+
/// Use when mmapping a file for the first time. When re-mapping a file
|
45
|
+
/// after expanding it the `reestablish` function should be used.
|
46
|
+
pub fn new(path: PathBuf, file: File) -> Result<Self> {
|
47
|
+
let stat = file.metadata().map_err(|e| {
|
48
|
+
MmapError::legacy(
|
49
|
+
format!("Can't stat {}: {e}", path.display()),
|
50
|
+
RubyError::Arg,
|
51
|
+
)
|
52
|
+
})?;
|
53
|
+
|
54
|
+
let file_size = util::cast_chk::<_, usize>(stat.len(), "file length")?;
|
55
|
+
|
56
|
+
// We need to ensure the underlying file descriptor is at least a page size.
|
57
|
+
// Otherwise, we could get a SIGBUS error if mmap() attempts to read or write
|
58
|
+
// past the file.
|
59
|
+
let reserve_size = Self::next_page_boundary(file_size)?;
|
60
|
+
|
61
|
+
// Cast: no-op.
|
62
|
+
Self::reserve_mmap_file_bytes(file.as_raw_fd(), reserve_size as off_t).map_err(|e| {
|
63
|
+
MmapError::legacy(
|
64
|
+
format!(
|
65
|
+
"Can't reserve {reserve_size} bytes for memory-mapped file in {}: {e}",
|
66
|
+
path.display()
|
67
|
+
),
|
68
|
+
RubyError::Io,
|
69
|
+
)
|
70
|
+
})?;
|
71
|
+
|
72
|
+
// Ensure we always have space for the header.
|
73
|
+
let map_len = file_size.max(HEADER_SIZE);
|
74
|
+
|
75
|
+
// SAFETY: There is the possibility of UB if the file is modified outside of
|
76
|
+
// this program.
|
77
|
+
let map = unsafe { MmapOptions::new().len(map_len).map_mut(&file) }.map_err(|e| {
|
78
|
+
MmapError::legacy(format!("mmap failed ({}): {e}", errno()), RubyError::Arg)
|
79
|
+
})?;
|
80
|
+
|
81
|
+
let len = file_size;
|
82
|
+
|
83
|
+
Ok(Self {
|
84
|
+
file,
|
85
|
+
path,
|
86
|
+
map,
|
87
|
+
len,
|
88
|
+
})
|
89
|
+
}
|
90
|
+
|
91
|
+
/// Re-mmap a file that was previously mapped.
|
92
|
+
pub fn reestablish(path: PathBuf, file: File, map_len: usize) -> Result<Self> {
|
93
|
+
// SAFETY: There is the possibility of UB if the file is modified outside of
|
94
|
+
// this program.
|
95
|
+
let map = unsafe { MmapOptions::new().len(map_len).map_mut(&file) }.map_err(|e| {
|
96
|
+
MmapError::legacy(format!("mmap failed ({}): {e}", errno()), RubyError::Arg)
|
97
|
+
})?;
|
98
|
+
|
99
|
+
// TODO should we keep this as the old len? We'd want to be able to truncate
|
100
|
+
// to the old length at this point if closing the file. Matching C implementation
|
101
|
+
// for now.
|
102
|
+
let len = map_len;
|
103
|
+
|
104
|
+
Ok(Self {
|
105
|
+
file,
|
106
|
+
path,
|
107
|
+
map,
|
108
|
+
len,
|
109
|
+
})
|
110
|
+
}
|
111
|
+
|
112
|
+
/// Add a new metrics entry to the end of the mmap. This will fail if the mmap is at
|
113
|
+
/// capacity. Callers must expand the file first.
|
114
|
+
///
|
115
|
+
/// SAFETY: Must not call any Ruby code for the lifetime of `key`, otherwise we risk
|
116
|
+
/// Ruby mutating the underlying `RString`.
|
117
|
+
pub unsafe fn initialize_entry(&mut self, key: &[u8], value: f64) -> Result<usize> {
|
118
|
+
// CAST: no-op on 32-bit, widening on 64-bit.
|
119
|
+
let current_used = self.load_used()? as usize;
|
120
|
+
let entry_length = RawEntry::calc_total_len(key.len())?;
|
121
|
+
|
122
|
+
let new_used = current_used.add_chk(entry_length)?;
|
123
|
+
|
124
|
+
// Increasing capacity requires expanding the file and re-mmapping it, we can't
|
125
|
+
// perform this from `InnerMmap`.
|
126
|
+
if self.capacity() < new_used {
|
127
|
+
return Err(MmapError::Other(format!(
|
128
|
+
"mmap capacity {} less than {}",
|
129
|
+
self.capacity(),
|
130
|
+
new_used
|
131
|
+
)));
|
132
|
+
}
|
133
|
+
|
134
|
+
let bytes = self.map.as_mut();
|
135
|
+
let value_offset = RawEntry::save(&mut bytes[current_used..new_used], key, value)?;
|
136
|
+
|
137
|
+
// Won't overflow as value_offset is less than new_used.
|
138
|
+
let position = current_used + value_offset;
|
139
|
+
let new_used32 = util::cast_chk::<_, u32>(new_used, "used")?;
|
140
|
+
|
141
|
+
self.save_used(new_used32)?;
|
142
|
+
Ok(position)
|
143
|
+
}
|
144
|
+
|
145
|
+
pub unsafe fn initialize_entry_exemplar(&mut self, key: &[u8], ex: Exemplar) -> Result<usize> {
|
146
|
+
// CAST: no-op on 32-bit, widening on 64-bit.
|
147
|
+
let current_used = self.load_used()? as usize;
|
148
|
+
let entry_length = RawEntry::calc_total_len_exemplar(key.len())?;
|
149
|
+
|
150
|
+
let new_used = current_used.add_chk(entry_length)?;
|
151
|
+
|
152
|
+
// Increasing capacity requires expanding the file and re-mmapping it, we can't
|
153
|
+
// perform this from `InnerMmap`.
|
154
|
+
if self.capacity() < new_used {
|
155
|
+
return Err(MmapError::Other(format!(
|
156
|
+
"mmap capacity {} less than {}",
|
157
|
+
self.capacity(),
|
158
|
+
new_used
|
159
|
+
)));
|
160
|
+
}
|
161
|
+
|
162
|
+
let bytes = self.map.as_mut();
|
163
|
+
let value_offset = RawEntry::save_exemplar(&mut bytes[current_used..new_used], key, ex)?;
|
164
|
+
|
165
|
+
// Won't overflow as value_offset is less than new_used.
|
166
|
+
let position = current_used + value_offset;
|
167
|
+
let new_used32 = util::cast_chk::<_, u32>(new_used, "used")?;
|
168
|
+
|
169
|
+
self.save_used(new_used32)?;
|
170
|
+
Ok(position)
|
171
|
+
}
|
172
|
+
|
173
|
+
pub fn save_exemplar(&mut self, offset: usize, exemplar: Exemplar) -> Result<()> {
|
174
|
+
if self.len.add_chk(size_of::<Exemplar>())? <= offset {
|
175
|
+
return Err(MmapError::out_of_bounds(
|
176
|
+
offset + size_of::<f64>(),
|
177
|
+
self.len,
|
178
|
+
));
|
179
|
+
}
|
180
|
+
|
181
|
+
if offset < HEADER_SIZE {
|
182
|
+
return Err(MmapError::Other(format!(
|
183
|
+
"writing to offset {offset} would overwrite file header"
|
184
|
+
)));
|
185
|
+
}
|
186
|
+
|
187
|
+
let val = serde_json::to_string(&exemplar).unwrap();
|
188
|
+
|
189
|
+
let value_bytes = val.as_bytes();
|
190
|
+
|
191
|
+
let mut value_bytes = value_bytes.to_vec();
|
192
|
+
value_bytes.extend(iter::repeat(0).take(EXEMPLAR_ENTRY_MAX_SIZE_BYTES - value_bytes.len()));
|
193
|
+
|
194
|
+
let value_range = self.item_range(offset, EXEMPLAR_ENTRY_MAX_SIZE_BYTES)?;
|
195
|
+
|
196
|
+
let bytes = self.map.as_mut();
|
197
|
+
bytes[value_range].copy_from_slice(&value_bytes);
|
198
|
+
|
199
|
+
Ok(())
|
200
|
+
}
|
201
|
+
|
202
|
+
/// Save a metrics value to an existing entry in the mmap.
|
203
|
+
pub fn save_value(&mut self, offset: usize, value: f64) -> Result<()> {
|
204
|
+
if self.len.add_chk(size_of::<f64>())? <= offset {
|
205
|
+
return Err(MmapError::out_of_bounds(
|
206
|
+
offset + size_of::<f64>(),
|
207
|
+
self.len,
|
208
|
+
));
|
209
|
+
}
|
210
|
+
|
211
|
+
if offset < HEADER_SIZE {
|
212
|
+
return Err(MmapError::Other(format!(
|
213
|
+
"writing to offset {offset} would overwrite file header"
|
214
|
+
)));
|
215
|
+
}
|
216
|
+
|
217
|
+
let value_bytes = value.to_ne_bytes();
|
218
|
+
let value_range = self.item_range(offset, value_bytes.len())?;
|
219
|
+
|
220
|
+
let bytes = self.map.as_mut();
|
221
|
+
bytes[value_range].copy_from_slice(&value_bytes);
|
222
|
+
|
223
|
+
Ok(())
|
224
|
+
}
|
225
|
+
|
226
|
+
/// Load a metrics value from an entry in the mmap.
|
227
|
+
pub fn load_value(&self, offset: usize) -> Result<f64> {
|
228
|
+
if self.len.add_chk(size_of::<f64>())? <= offset {
|
229
|
+
return Err(MmapError::out_of_bounds(
|
230
|
+
offset + size_of::<f64>(),
|
231
|
+
self.len,
|
232
|
+
));
|
233
|
+
}
|
234
|
+
read_f64(self.map.as_ref(), offset)
|
235
|
+
}
|
236
|
+
|
237
|
+
pub fn load_exemplar(&mut self, offset: usize) -> Result<Exemplar> {
|
238
|
+
if self.len.add_chk(EXEMPLAR_ENTRY_MAX_SIZE_BYTES)? <= offset {
|
239
|
+
return Err(MmapError::out_of_bounds(
|
240
|
+
offset + EXEMPLAR_ENTRY_MAX_SIZE_BYTES,
|
241
|
+
self.len,
|
242
|
+
));
|
243
|
+
}
|
244
|
+
|
245
|
+
read_exemplar(self.map.as_mut(), offset)
|
246
|
+
}
|
247
|
+
|
248
|
+
/// The length of data written to the file.
|
249
|
+
/// With a new file this is only set when Ruby calls `slice` on
|
250
|
+
/// `FastMmapedFileRs`, so even if data has been written to the
|
251
|
+
/// mmap attempts to read will fail until a String is created.
|
252
|
+
/// When an existing file is read we set this value immediately.
|
253
|
+
///
|
254
|
+
/// Equivalent to `i_mm->t->real` in the C implementation.
|
255
|
+
#[inline]
|
256
|
+
pub fn len(&self) -> usize {
|
257
|
+
self.len
|
258
|
+
}
|
259
|
+
|
260
|
+
/// The total length in bytes of the mmapped file.
|
261
|
+
///
|
262
|
+
/// Equivalent to `i_mm->t->len` in the C implementation.
|
263
|
+
#[inline]
|
264
|
+
pub fn capacity(&self) -> usize {
|
265
|
+
self.map.len()
|
266
|
+
}
|
267
|
+
|
268
|
+
/// Update the length of the mmap considered to be written.
|
269
|
+
pub fn set_len(&mut self, len: usize) {
|
270
|
+
self.len = len;
|
271
|
+
}
|
272
|
+
|
273
|
+
/// Returns a raw pointer to the mmap.
|
274
|
+
pub fn as_ptr(&self) -> *const u8 {
|
275
|
+
self.map.as_ptr()
|
276
|
+
}
|
277
|
+
|
278
|
+
/// Returns a mutable raw pointer to the mmap.
|
279
|
+
/// For use in updating RString internals which requires a mutable pointer.
|
280
|
+
pub fn as_mut_ptr(&self) -> *mut u8 {
|
281
|
+
self.map.as_ptr().cast_mut()
|
282
|
+
}
|
283
|
+
|
284
|
+
/// Perform an msync(2) on the mmap, flushing all changes written
|
285
|
+
/// to disk. The sync may optionally be performed asynchronously.
|
286
|
+
pub fn flush(&mut self, f_async: bool) -> Result<()> {
|
287
|
+
if f_async {
|
288
|
+
self.map
|
289
|
+
.flush_async()
|
290
|
+
.map_err(|_| MmapError::legacy(format!("msync({})", errno()), RubyError::Arg))
|
291
|
+
} else {
|
292
|
+
self.map
|
293
|
+
.flush()
|
294
|
+
.map_err(|_| MmapError::legacy(format!("msync({})", errno()), RubyError::Arg))
|
295
|
+
}
|
296
|
+
}
|
297
|
+
|
298
|
+
/// Load the `used` header containing the size of the metrics data written.
|
299
|
+
pub fn load_used(&self) -> Result<u32> {
|
300
|
+
match read_u32(self.map.as_ref(), 0) {
|
301
|
+
// CAST: we know HEADER_SIZE fits in a u32.
|
302
|
+
Ok(0) => Ok(HEADER_SIZE as u32),
|
303
|
+
u => u,
|
304
|
+
}
|
305
|
+
}
|
306
|
+
|
307
|
+
/// Update the `used` header to the value provided.
|
308
|
+
/// value provided.
|
309
|
+
pub fn save_used(&mut self, used: u32) -> Result<()> {
|
310
|
+
let bytes = self.map.as_mut();
|
311
|
+
bytes[..size_of::<u32>()].copy_from_slice(&used.to_ne_bytes());
|
312
|
+
|
313
|
+
Ok(())
|
314
|
+
}
|
315
|
+
|
316
|
+
/// Drop self, which performs an munmap(2) on the mmap,
|
317
|
+
/// returning the open `File` and `PathBuf` so the
|
318
|
+
/// caller can expand the file and re-mmap it.
|
319
|
+
pub fn munmap(self) -> (File, PathBuf) {
|
320
|
+
(self.file, self.path)
|
321
|
+
}
|
322
|
+
|
323
|
+
// From https://stackoverflow.com/a/22820221: The difference with
|
324
|
+
// ftruncate(2) is that (on file systems supporting it, e.g. Ext4)
|
325
|
+
// disk space is indeed reserved by posix_fallocate but ftruncate
|
326
|
+
// extends the file by adding holes (and without reserving disk
|
327
|
+
// space).
|
328
|
+
#[cfg(target_os = "linux")]
|
329
|
+
fn reserve_mmap_file_bytes(fd: RawFd, len: off_t) -> nix::Result<()> {
|
330
|
+
nix::fcntl::posix_fallocate(fd, 0, len)
|
331
|
+
}
|
332
|
+
|
333
|
+
// We simplify the reference implementation since we generally
|
334
|
+
// don't need to reserve more than a page size.
|
335
|
+
#[cfg(not(target_os = "linux"))]
|
336
|
+
fn reserve_mmap_file_bytes(fd: RawFd, len: off_t) -> nix::Result<()> {
|
337
|
+
nix::unistd::ftruncate(fd, len)
|
338
|
+
}
|
339
|
+
|
340
|
+
fn item_range(&self, start: usize, len: usize) -> Result<Range<usize>> {
|
341
|
+
let offset_end = start.add_chk(len)?;
|
342
|
+
|
343
|
+
if offset_end >= self.capacity() {
|
344
|
+
return Err(MmapError::out_of_bounds(offset_end, self.capacity()));
|
345
|
+
}
|
346
|
+
|
347
|
+
Ok(start..offset_end)
|
348
|
+
}
|
349
|
+
|
350
|
+
fn next_page_boundary(len: usize) -> Result<c_long> {
|
351
|
+
use nix::unistd::{self, SysconfVar};
|
352
|
+
|
353
|
+
let len = c_long::try_from(len)
|
354
|
+
.map_err(|_| MmapError::failed_cast::<_, c_long>(len, "file len"))?;
|
355
|
+
|
356
|
+
let mut page_size = match unistd::sysconf(SysconfVar::PAGE_SIZE) {
|
357
|
+
Ok(Some(p)) if p > 0 => p,
|
358
|
+
Ok(Some(p)) => {
|
359
|
+
return Err(MmapError::legacy(
|
360
|
+
format!("Invalid page size {p}"),
|
361
|
+
RubyError::Io,
|
362
|
+
))
|
363
|
+
}
|
364
|
+
Ok(None) => {
|
365
|
+
return Err(MmapError::legacy(
|
366
|
+
"No system page size found",
|
367
|
+
RubyError::Io,
|
368
|
+
))
|
369
|
+
}
|
370
|
+
Err(_) => {
|
371
|
+
return Err(MmapError::legacy(
|
372
|
+
"Failed to get system page size: {e}",
|
373
|
+
RubyError::Io,
|
374
|
+
))
|
375
|
+
}
|
376
|
+
};
|
377
|
+
|
378
|
+
while page_size < len {
|
379
|
+
page_size = page_size.mul_chk(2)?;
|
380
|
+
}
|
381
|
+
|
382
|
+
Ok(page_size)
|
383
|
+
}
|
384
|
+
}
|
385
|
+
|
386
|
+
#[cfg(test)]
|
387
|
+
mod test {
|
388
|
+
use nix::unistd::{self, SysconfVar};
|
389
|
+
|
390
|
+
use super::*;
|
391
|
+
use crate::testhelper::{self, TestEntry, TestFile};
|
392
|
+
use crate::HEADER_SIZE;
|
393
|
+
|
394
|
+
#[test]
|
395
|
+
fn test_new() {
|
396
|
+
struct TestCase {
|
397
|
+
name: &'static str,
|
398
|
+
existing: bool,
|
399
|
+
expected_len: usize,
|
400
|
+
}
|
401
|
+
|
402
|
+
let page_size = unistd::sysconf(SysconfVar::PAGE_SIZE).unwrap().unwrap();
|
403
|
+
|
404
|
+
let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
|
405
|
+
let value = 1.0;
|
406
|
+
let entry_len = TestEntry::new(json, value).as_bytes().len();
|
407
|
+
|
408
|
+
let tc = vec![
|
409
|
+
TestCase {
|
410
|
+
name: "empty file",
|
411
|
+
existing: false,
|
412
|
+
expected_len: 0,
|
413
|
+
},
|
414
|
+
TestCase {
|
415
|
+
name: "existing file",
|
416
|
+
existing: true,
|
417
|
+
expected_len: HEADER_SIZE + entry_len,
|
418
|
+
},
|
419
|
+
];
|
420
|
+
|
421
|
+
for case in tc {
|
422
|
+
let name = case.name;
|
423
|
+
|
424
|
+
let data = match case.existing {
|
425
|
+
true => testhelper::entries_to_db(&[json], &[1.0], None),
|
426
|
+
false => Vec::new(),
|
427
|
+
};
|
428
|
+
|
429
|
+
let TestFile {
|
430
|
+
file: original_file,
|
431
|
+
path,
|
432
|
+
dir: _dir,
|
433
|
+
} = TestFile::new(&data);
|
434
|
+
|
435
|
+
let original_stat = original_file.metadata().unwrap();
|
436
|
+
|
437
|
+
let inner = InnerMmap::new(path.clone(), original_file).unwrap();
|
438
|
+
|
439
|
+
let updated_file = File::open(&path).unwrap();
|
440
|
+
let updated_stat = updated_file.metadata().unwrap();
|
441
|
+
|
442
|
+
assert!(
|
443
|
+
updated_stat.len() > original_stat.len(),
|
444
|
+
"test case: {name} - file has been extended"
|
445
|
+
);
|
446
|
+
|
447
|
+
assert_eq!(
|
448
|
+
updated_stat.len(),
|
449
|
+
page_size as u64,
|
450
|
+
"test case: {name} - file extended to page size"
|
451
|
+
);
|
452
|
+
|
453
|
+
assert_eq!(
|
454
|
+
inner.capacity() as u64,
|
455
|
+
original_stat.len().max(HEADER_SIZE as u64),
|
456
|
+
"test case: {name} - mmap capacity matches original file len, unless smaller than HEADER_SIZE"
|
457
|
+
);
|
458
|
+
|
459
|
+
assert_eq!(
|
460
|
+
case.expected_len,
|
461
|
+
inner.len(),
|
462
|
+
"test case: {name} - len set"
|
463
|
+
);
|
464
|
+
}
|
465
|
+
}
|
466
|
+
|
467
|
+
#[test]
|
468
|
+
fn test_reestablish() {
|
469
|
+
struct TestCase {
|
470
|
+
name: &'static str,
|
471
|
+
target_len: usize,
|
472
|
+
expected_len: usize,
|
473
|
+
}
|
474
|
+
|
475
|
+
let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
|
476
|
+
|
477
|
+
let tc = vec![TestCase {
|
478
|
+
name: "ok",
|
479
|
+
target_len: 4096,
|
480
|
+
expected_len: 4096,
|
481
|
+
}];
|
482
|
+
|
483
|
+
for case in tc {
|
484
|
+
let name = case.name;
|
485
|
+
|
486
|
+
let data = testhelper::entries_to_db(&[json], &[1.0], None);
|
487
|
+
|
488
|
+
let TestFile {
|
489
|
+
file: original_file,
|
490
|
+
path,
|
491
|
+
dir: _dir,
|
492
|
+
} = TestFile::new(&data);
|
493
|
+
|
494
|
+
let inner =
|
495
|
+
InnerMmap::reestablish(path.clone(), original_file, case.target_len).unwrap();
|
496
|
+
|
497
|
+
assert_eq!(
|
498
|
+
case.target_len,
|
499
|
+
inner.capacity(),
|
500
|
+
"test case: {name} - mmap capacity set to target len",
|
501
|
+
);
|
502
|
+
|
503
|
+
assert_eq!(
|
504
|
+
case.expected_len,
|
505
|
+
inner.len(),
|
506
|
+
"test case: {name} - len set"
|
507
|
+
);
|
508
|
+
}
|
509
|
+
}
|
510
|
+
|
511
|
+
#[test]
|
512
|
+
fn test_initialize_entry() {
|
513
|
+
struct TestCase {
|
514
|
+
name: &'static str,
|
515
|
+
empty: bool,
|
516
|
+
used: Option<u32>,
|
517
|
+
expected_used: Option<u32>,
|
518
|
+
expected_value_offset: Option<usize>,
|
519
|
+
expected_err: Option<MmapError>,
|
520
|
+
}
|
521
|
+
|
522
|
+
let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
|
523
|
+
let value = 1.0;
|
524
|
+
let entry_len = TestEntry::new(json, value).as_bytes().len();
|
525
|
+
|
526
|
+
let tc = vec![
|
527
|
+
TestCase {
|
528
|
+
name: "empty file, not expanded by outer mmap",
|
529
|
+
empty: true,
|
530
|
+
used: None,
|
531
|
+
expected_used: None,
|
532
|
+
expected_value_offset: None,
|
533
|
+
expected_err: Some(MmapError::Other(format!(
|
534
|
+
"mmap capacity {HEADER_SIZE} less than {}",
|
535
|
+
entry_len + HEADER_SIZE,
|
536
|
+
))),
|
537
|
+
},
|
538
|
+
TestCase {
|
539
|
+
name: "data in file",
|
540
|
+
empty: false,
|
541
|
+
used: None,
|
542
|
+
expected_used: Some(HEADER_SIZE as u32 + (entry_len * 2) as u32),
|
543
|
+
expected_value_offset: Some(176),
|
544
|
+
expected_err: None,
|
545
|
+
},
|
546
|
+
TestCase {
|
547
|
+
name: "data in file, invalid used larger than file",
|
548
|
+
empty: false,
|
549
|
+
used: Some(10_000),
|
550
|
+
expected_used: None,
|
551
|
+
expected_value_offset: None,
|
552
|
+
expected_err: Some(MmapError::Other(format!(
|
553
|
+
"mmap capacity 4096 less than {}",
|
554
|
+
10_000 + entry_len
|
555
|
+
))),
|
556
|
+
},
|
557
|
+
];
|
558
|
+
|
559
|
+
for case in tc {
|
560
|
+
let name = case.name;
|
561
|
+
|
562
|
+
let data = match case.empty {
|
563
|
+
true => Vec::new(),
|
564
|
+
false => testhelper::entries_to_db(&[json], &[1.0], case.used),
|
565
|
+
};
|
566
|
+
|
567
|
+
let TestFile {
|
568
|
+
file,
|
569
|
+
path,
|
570
|
+
dir: _dir,
|
571
|
+
} = TestFile::new(&data);
|
572
|
+
|
573
|
+
if !case.empty {
|
574
|
+
// Ensure the file is large enough to have additional entries added.
|
575
|
+
// Normally the outer mmap handles this.
|
576
|
+
file.set_len(4096).unwrap();
|
577
|
+
}
|
578
|
+
let mut inner = InnerMmap::new(path, file).unwrap();
|
579
|
+
|
580
|
+
let result = unsafe { inner.initialize_entry(json.as_bytes(), value) };
|
581
|
+
|
582
|
+
if let Some(expected_used) = case.expected_used {
|
583
|
+
assert_eq!(
|
584
|
+
expected_used,
|
585
|
+
inner.load_used().unwrap(),
|
586
|
+
"test case: {name} - used"
|
587
|
+
);
|
588
|
+
}
|
589
|
+
|
590
|
+
if let Some(expected_value_offset) = case.expected_value_offset {
|
591
|
+
assert_eq!(
|
592
|
+
expected_value_offset,
|
593
|
+
*result.as_ref().unwrap(),
|
594
|
+
"test case: {name} - value_offset"
|
595
|
+
);
|
596
|
+
}
|
597
|
+
|
598
|
+
if let Some(expected_err) = case.expected_err {
|
599
|
+
assert_eq!(
|
600
|
+
expected_err,
|
601
|
+
result.unwrap_err(),
|
602
|
+
"test case: {name} - error"
|
603
|
+
);
|
604
|
+
}
|
605
|
+
}
|
606
|
+
}
|
607
|
+
|
608
|
+
#[test]
|
609
|
+
fn test_save_value() {
|
610
|
+
let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
|
611
|
+
let value = 1.0;
|
612
|
+
let upper_bound = TestEntry::new(json, value).as_bytes().len() + HEADER_SIZE;
|
613
|
+
let value_offset = upper_bound - size_of::<f64>();
|
614
|
+
|
615
|
+
struct TestCase {
|
616
|
+
name: &'static str,
|
617
|
+
empty: bool,
|
618
|
+
len: Option<usize>,
|
619
|
+
offset: usize,
|
620
|
+
expected_err: Option<MmapError>,
|
621
|
+
}
|
622
|
+
|
623
|
+
let tc = vec![
|
624
|
+
TestCase {
|
625
|
+
name: "existing file, in bounds",
|
626
|
+
empty: false,
|
627
|
+
len: None,
|
628
|
+
offset: upper_bound - size_of::<f64>() - 1,
|
629
|
+
expected_err: None,
|
630
|
+
},
|
631
|
+
TestCase {
|
632
|
+
name: "existing file, out of bounds",
|
633
|
+
empty: false,
|
634
|
+
len: Some(100),
|
635
|
+
offset: upper_bound * 2,
|
636
|
+
expected_err: Some(MmapError::out_of_bounds(
|
637
|
+
upper_bound * 2 + size_of::<f64>(),
|
638
|
+
100,
|
639
|
+
)),
|
640
|
+
},
|
641
|
+
TestCase {
|
642
|
+
name: "existing file, off by one",
|
643
|
+
empty: false,
|
644
|
+
len: None,
|
645
|
+
offset: value_offset + 1,
|
646
|
+
expected_err: Some(MmapError::out_of_bounds(
|
647
|
+
value_offset + 1 + size_of::<f64>(),
|
648
|
+
upper_bound,
|
649
|
+
)),
|
650
|
+
},
|
651
|
+
TestCase {
|
652
|
+
name: "empty file cannot be saved to",
|
653
|
+
empty: true,
|
654
|
+
len: None,
|
655
|
+
offset: 8,
|
656
|
+
expected_err: Some(MmapError::out_of_bounds(8 + size_of::<f64>(), 0)),
|
657
|
+
},
|
658
|
+
TestCase {
|
659
|
+
name: "overwrite header",
|
660
|
+
empty: false,
|
661
|
+
len: None,
|
662
|
+
offset: 7,
|
663
|
+
expected_err: Some(MmapError::Other(
|
664
|
+
"writing to offset 7 would overwrite file header".to_string(),
|
665
|
+
)),
|
666
|
+
},
|
667
|
+
];
|
668
|
+
|
669
|
+
for case in tc {
|
670
|
+
let name = case.name;
|
671
|
+
|
672
|
+
let mut data = match case.empty {
|
673
|
+
true => Vec::new(),
|
674
|
+
false => testhelper::entries_to_db(&[json], &[1.0], None),
|
675
|
+
};
|
676
|
+
|
677
|
+
if let Some(len) = case.len {
|
678
|
+
// Pad input to desired length.
|
679
|
+
data.append(&mut vec![0xff; len - upper_bound]);
|
680
|
+
}
|
681
|
+
|
682
|
+
let TestFile {
|
683
|
+
file,
|
684
|
+
path,
|
685
|
+
dir: _dir,
|
686
|
+
} = TestFile::new(&data);
|
687
|
+
|
688
|
+
let mut inner = InnerMmap::new(path, file).unwrap();
|
689
|
+
|
690
|
+
let result = inner.save_value(case.offset, value);
|
691
|
+
|
692
|
+
if let Some(expected_err) = case.expected_err {
|
693
|
+
assert_eq!(
|
694
|
+
expected_err,
|
695
|
+
result.unwrap_err(),
|
696
|
+
"test case: {name} - expected err"
|
697
|
+
);
|
698
|
+
} else {
|
699
|
+
assert!(result.is_ok(), "test case: {name} - success");
|
700
|
+
|
701
|
+
assert_eq!(
|
702
|
+
value,
|
703
|
+
util::read_f64(&inner.map, case.offset).unwrap(),
|
704
|
+
"test case: {name} - value saved"
|
705
|
+
);
|
706
|
+
}
|
707
|
+
}
|
708
|
+
}
|
709
|
+
|
710
|
+
#[test]
|
711
|
+
fn test_load_value() {
|
712
|
+
let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
|
713
|
+
let value = 1.0;
|
714
|
+
let total_len = TestEntry::new(json, value).as_bytes().len() + HEADER_SIZE;
|
715
|
+
let value_offset = total_len - size_of::<f64>();
|
716
|
+
|
717
|
+
struct TestCase {
|
718
|
+
name: &'static str,
|
719
|
+
offset: usize,
|
720
|
+
expected_err: Option<MmapError>,
|
721
|
+
}
|
722
|
+
|
723
|
+
let tc = vec![
|
724
|
+
TestCase {
|
725
|
+
name: "in bounds",
|
726
|
+
offset: value_offset,
|
727
|
+
expected_err: None,
|
728
|
+
},
|
729
|
+
TestCase {
|
730
|
+
name: "out of bounds",
|
731
|
+
offset: value_offset * 2,
|
732
|
+
expected_err: Some(MmapError::out_of_bounds(
|
733
|
+
value_offset * 2 + size_of::<f64>(),
|
734
|
+
total_len,
|
735
|
+
)),
|
736
|
+
},
|
737
|
+
TestCase {
|
738
|
+
name: "off by one",
|
739
|
+
offset: value_offset + 1,
|
740
|
+
expected_err: Some(MmapError::out_of_bounds(
|
741
|
+
value_offset + 1 + size_of::<f64>(),
|
742
|
+
total_len,
|
743
|
+
)),
|
744
|
+
},
|
745
|
+
];
|
746
|
+
|
747
|
+
for case in tc {
|
748
|
+
let name = case.name;
|
749
|
+
|
750
|
+
let data = testhelper::entries_to_db(&[json], &[1.0], None);
|
751
|
+
|
752
|
+
let TestFile {
|
753
|
+
file,
|
754
|
+
path,
|
755
|
+
dir: _dir,
|
756
|
+
} = TestFile::new(&data);
|
757
|
+
|
758
|
+
let inner = InnerMmap::new(path, file).unwrap();
|
759
|
+
|
760
|
+
let result = inner.load_value(case.offset);
|
761
|
+
|
762
|
+
if let Some(expected_err) = case.expected_err {
|
763
|
+
assert_eq!(
|
764
|
+
expected_err,
|
765
|
+
result.unwrap_err(),
|
766
|
+
"test case: {name} - expected err"
|
767
|
+
);
|
768
|
+
} else {
|
769
|
+
assert!(result.is_ok(), "test case: {name} - success");
|
770
|
+
|
771
|
+
assert_eq!(value, result.unwrap(), "test case: {name} - value loaded");
|
772
|
+
}
|
773
|
+
}
|
774
|
+
}
|
775
|
+
}
|