vinted-prometheus-client-mmap 1.5.0-x86_64-linux

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +5 -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 +1252 -0
  10. data/ext/fast_mmaped_file_rs/src/file_info.rs +240 -0
  11. data/ext/fast_mmaped_file_rs/src/lib.rs +89 -0
  12. data/ext/fast_mmaped_file_rs/src/macros.rs +14 -0
  13. data/ext/fast_mmaped_file_rs/src/map.rs +519 -0
  14. data/ext/fast_mmaped_file_rs/src/metrics.proto +153 -0
  15. data/ext/fast_mmaped_file_rs/src/mmap/inner.rs +775 -0
  16. data/ext/fast_mmaped_file_rs/src/mmap.rs +977 -0
  17. data/ext/fast_mmaped_file_rs/src/raw_entry.rs +547 -0
  18. data/ext/fast_mmaped_file_rs/src/testhelper.rs +222 -0
  19. data/ext/fast_mmaped_file_rs/src/util.rs +140 -0
  20. data/lib/.DS_Store +0 -0
  21. data/lib/2.7/fast_mmaped_file_rs.so +0 -0
  22. data/lib/3.0/fast_mmaped_file_rs.so +0 -0
  23. data/lib/3.1/fast_mmaped_file_rs.so +0 -0
  24. data/lib/3.2/fast_mmaped_file_rs.so +0 -0
  25. data/lib/3.3/fast_mmaped_file_rs.so +0 -0
  26. data/lib/prometheus/.DS_Store +0 -0
  27. data/lib/prometheus/client/configuration.rb +24 -0
  28. data/lib/prometheus/client/counter.rb +27 -0
  29. data/lib/prometheus/client/formats/protobuf.rb +93 -0
  30. data/lib/prometheus/client/formats/text.rb +85 -0
  31. data/lib/prometheus/client/gauge.rb +40 -0
  32. data/lib/prometheus/client/helper/entry_parser.rb +132 -0
  33. data/lib/prometheus/client/helper/file_locker.rb +50 -0
  34. data/lib/prometheus/client/helper/json_parser.rb +23 -0
  35. data/lib/prometheus/client/helper/metrics_processing.rb +45 -0
  36. data/lib/prometheus/client/helper/metrics_representation.rb +51 -0
  37. data/lib/prometheus/client/helper/mmaped_file.rb +64 -0
  38. data/lib/prometheus/client/helper/plain_file.rb +29 -0
  39. data/lib/prometheus/client/histogram.rb +80 -0
  40. data/lib/prometheus/client/label_set_validator.rb +85 -0
  41. data/lib/prometheus/client/metric.rb +80 -0
  42. data/lib/prometheus/client/mmaped_dict.rb +83 -0
  43. data/lib/prometheus/client/mmaped_value.rb +164 -0
  44. data/lib/prometheus/client/page_size.rb +17 -0
  45. data/lib/prometheus/client/push.rb +203 -0
  46. data/lib/prometheus/client/rack/collector.rb +88 -0
  47. data/lib/prometheus/client/rack/exporter.rb +102 -0
  48. data/lib/prometheus/client/registry.rb +65 -0
  49. data/lib/prometheus/client/simple_value.rb +31 -0
  50. data/lib/prometheus/client/summary.rb +69 -0
  51. data/lib/prometheus/client/support/puma.rb +44 -0
  52. data/lib/prometheus/client/support/unicorn.rb +35 -0
  53. data/lib/prometheus/client/uses_value_type.rb +20 -0
  54. data/lib/prometheus/client/version.rb +5 -0
  55. data/lib/prometheus/client.rb +58 -0
  56. data/lib/prometheus.rb +3 -0
  57. 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
+ }