vinted-prometheus-client-mmap 1.2.1

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