prometheus-client-mmap 0.19.1 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,473 @@
1
+ use std::mem::size_of;
2
+
3
+ use crate::error::MmapError;
4
+ use crate::util;
5
+ use crate::util::CheckedOps;
6
+ use crate::Result;
7
+
8
+ /// The logic to save a `MetricsEntry`, or parse one from a byte slice.
9
+ #[derive(PartialEq, Clone, Debug)]
10
+ pub struct RawEntry<'a> {
11
+ bytes: &'a [u8],
12
+ encoded_len: usize,
13
+ }
14
+
15
+ impl<'a> RawEntry<'a> {
16
+ /// Save an entry to the mmap, returning the value offset in the newly created entry.
17
+ pub fn save(bytes: &'a mut [u8], key: &[u8], value: f64) -> Result<usize> {
18
+ let total_len = Self::calc_total_len(key.len())?;
19
+
20
+ if total_len > bytes.len() {
21
+ return Err(MmapError::Other(format!(
22
+ "entry length {total_len} larger than slice length {}",
23
+ bytes.len()
24
+ )));
25
+ }
26
+
27
+ // CAST: `calc_len` runs `check_encoded_len`, we know the key len
28
+ // is less than i32::MAX. No risk of overflows or failed casts.
29
+ let key_len: u32 = key.len() as u32;
30
+
31
+ // Write the key length to the mmap.
32
+ bytes[..size_of::<u32>()].copy_from_slice(&key_len.to_ne_bytes());
33
+
34
+ // Advance slice past the size.
35
+ let bytes = &mut bytes[size_of::<u32>()..];
36
+
37
+ bytes[..key.len()].copy_from_slice(key);
38
+
39
+ // Advance to end of key.
40
+ let bytes = &mut bytes[key.len()..];
41
+
42
+ let pad_len = Self::padding_len(key.len());
43
+ bytes[..pad_len].fill(b' ');
44
+ let bytes = &mut bytes[pad_len..];
45
+
46
+ bytes[..size_of::<f64>()].copy_from_slice(&value.to_ne_bytes());
47
+
48
+ Self::calc_value_offset(key.len())
49
+ }
50
+
51
+ /// Parse a byte slice starting into an `MmapEntry`.
52
+ pub fn from_slice(bytes: &'a [u8]) -> Result<Self> {
53
+ // CAST: no-op on 32-bit, widening on 64-bit.
54
+ let encoded_len = util::read_u32(bytes, 0)? as usize;
55
+
56
+ let total_len = Self::calc_total_len(encoded_len)?;
57
+
58
+ // Confirm the value is in bounds of the slice provided.
59
+ if total_len > bytes.len() {
60
+ return Err(MmapError::out_of_bounds(total_len, bytes.len()));
61
+ }
62
+
63
+ // Advance slice past length int and cut at end of entry.
64
+ let bytes = &bytes[size_of::<u32>()..total_len];
65
+
66
+ Ok(Self { bytes, encoded_len })
67
+ }
68
+
69
+ /// Read the `f64` value of an entry from memory.
70
+ #[inline]
71
+ pub fn value(&self) -> f64 {
72
+ // We've stripped off the leading u32, don't include that here.
73
+ let offset = self.encoded_len + Self::padding_len(self.encoded_len);
74
+
75
+ // UNWRAP: We confirm in the constructor that the value offset
76
+ // is in-range for the slice.
77
+ util::read_f64(self.bytes, offset).unwrap()
78
+ }
79
+
80
+ /// The length of the entry key without padding.
81
+ #[inline]
82
+ pub fn encoded_len(&self) -> usize {
83
+ self.encoded_len
84
+ }
85
+
86
+ /// Returns a slice with the JSON string in the entry, excluding padding.
87
+ #[inline]
88
+ pub fn json(&self) -> &[u8] {
89
+ &self.bytes[..self.encoded_len]
90
+ }
91
+
92
+ /// Calculate the total length of an `MmapEntry`, including the string length,
93
+ /// string, padding, and value.
94
+ #[inline]
95
+ pub fn total_len(&self) -> usize {
96
+ // UNWRAP:: We confirmed in the constructor that this doesn't overflow.
97
+ Self::calc_total_len(self.encoded_len).unwrap()
98
+ }
99
+
100
+ /// Calculate the total length of an `MmapEntry`, including the string length,
101
+ /// string, padding, and value. Validates encoding_len is within expected bounds.
102
+ #[inline]
103
+ pub fn calc_total_len(encoded_len: usize) -> Result<usize> {
104
+ Self::calc_value_offset(encoded_len)?.add_chk(size_of::<f64>())
105
+ }
106
+
107
+ /// Calculate the value offset of an `MmapEntry`, including the string length,
108
+ /// string, padding. Validates encoding_len is within expected bounds.
109
+ #[inline]
110
+ pub fn calc_value_offset(encoded_len: usize) -> Result<usize> {
111
+ Self::check_encoded_len(encoded_len)?;
112
+
113
+ Ok(size_of::<u32>() + encoded_len + Self::padding_len(encoded_len))
114
+ }
115
+
116
+ /// Calculate the number of padding bytes to add to the value key to reach
117
+ /// 8-byte alignment. Does not validate key length.
118
+ #[inline]
119
+ pub fn padding_len(encoded_len: usize) -> usize {
120
+ 8 - (size_of::<u32>() + encoded_len) % 8
121
+ }
122
+
123
+ #[inline]
124
+ fn check_encoded_len(encoded_len: usize) -> Result<()> {
125
+ if encoded_len as u64 > i32::MAX as u64 {
126
+ return Err(MmapError::KeyLength);
127
+ }
128
+ Ok(())
129
+ }
130
+ }
131
+
132
+ #[cfg(test)]
133
+ mod test {
134
+ use bstr::ByteSlice;
135
+
136
+ use super::*;
137
+ use crate::testhelper::TestEntry;
138
+
139
+ #[test]
140
+ fn test_from_slice() {
141
+ #[derive(PartialEq, Default, Debug)]
142
+ struct TestCase {
143
+ name: &'static str,
144
+ input: TestEntry,
145
+ expected_enc_len: Option<usize>,
146
+ expected_err: Option<MmapError>,
147
+ }
148
+
149
+ let tc = vec![
150
+ TestCase {
151
+ name: "ok",
152
+ input: TestEntry {
153
+ header: 61,
154
+ json: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
155
+ padding_len: 7,
156
+ value: 1.0,
157
+ },
158
+ expected_enc_len: Some(61),
159
+ ..Default::default()
160
+ },
161
+ TestCase {
162
+ name: "zero length key",
163
+ input: TestEntry {
164
+ header: 0,
165
+ json: "",
166
+ padding_len: 4,
167
+ value: 1.0,
168
+ },
169
+ expected_enc_len: Some(0),
170
+ ..Default::default()
171
+ },
172
+ TestCase {
173
+ name: "header value too large",
174
+ input: TestEntry {
175
+ header: i32::MAX as u32 + 1,
176
+ json: "foo",
177
+ padding_len: 1,
178
+ value: 0.0,
179
+ },
180
+ expected_err: Some(MmapError::KeyLength),
181
+ ..Default::default()
182
+ },
183
+ TestCase {
184
+ name: "header value much longer than json len",
185
+ input: TestEntry {
186
+ header: 256,
187
+ json: "foobar",
188
+ padding_len: 6,
189
+ value: 1.0,
190
+ },
191
+ expected_err: Some(MmapError::out_of_bounds(272, 24)),
192
+ ..Default::default()
193
+ },
194
+ TestCase {
195
+ // Situations where encoded_len is wrong but padding makes the
196
+ // value offset the same are not caught.
197
+ name: "header off by one",
198
+ input: TestEntry {
199
+ header: 4,
200
+ json: "123",
201
+ padding_len: 1,
202
+ value: 1.0,
203
+ },
204
+ expected_err: Some(MmapError::out_of_bounds(24, 16)),
205
+ ..Default::default()
206
+ },
207
+ ];
208
+
209
+ for case in tc {
210
+ let name = case.name;
211
+ let input = case.input.as_bstring();
212
+
213
+ let resp = RawEntry::from_slice(&input);
214
+
215
+ if case.expected_err.is_none() {
216
+ let expected_buf = case.input.as_bytes_no_header();
217
+ let resp = resp.as_ref().unwrap();
218
+ let bytes = resp.bytes;
219
+
220
+ assert_eq!(expected_buf, bytes.as_bstr(), "test case: {name} - bytes",);
221
+
222
+ assert_eq!(
223
+ resp.json(),
224
+ case.input.json.as_bytes(),
225
+ "test case: {name} - json matches"
226
+ );
227
+
228
+ assert_eq!(
229
+ resp.total_len(),
230
+ case.input.as_bstring().len(),
231
+ "test case: {name} - total_len matches"
232
+ );
233
+
234
+ assert_eq!(
235
+ resp.encoded_len(),
236
+ case.input.json.len(),
237
+ "test case: {name} - encoded_len matches"
238
+ );
239
+
240
+ assert!(
241
+ resp.json().iter().all(|&c| c != b' '),
242
+ "test case: {name} - no spaces in json"
243
+ );
244
+
245
+ let padding_len = RawEntry::padding_len(case.input.json.len());
246
+ assert!(
247
+ bytes[resp.encoded_len..resp.encoded_len + padding_len]
248
+ .iter()
249
+ .all(|&c| c == b' '),
250
+ "test case: {name} - padding is spaces"
251
+ );
252
+
253
+ assert_eq!(
254
+ resp.value(),
255
+ case.input.value,
256
+ "test case: {name} - value is correct"
257
+ );
258
+ }
259
+
260
+ if let Some(expected_enc_len) = case.expected_enc_len {
261
+ assert_eq!(
262
+ expected_enc_len,
263
+ resp.as_ref().unwrap().encoded_len,
264
+ "test case: {name} - encoded len",
265
+ );
266
+ }
267
+
268
+ if let Some(expected_err) = case.expected_err {
269
+ assert_eq!(expected_err, resp.unwrap_err(), "test case: {name} - error",);
270
+ }
271
+ }
272
+ }
273
+
274
+ #[test]
275
+ fn test_save() {
276
+ struct TestCase {
277
+ name: &'static str,
278
+ key: &'static [u8],
279
+ value: f64,
280
+ buf_len: usize,
281
+ expected_entry: Option<TestEntry>,
282
+ expected_resp: Result<usize>,
283
+ }
284
+
285
+ // TODO No test case to validate keys with len > i32::MAX, adding a static that large crashes
286
+ // the test binary.
287
+ let tc = vec![
288
+ TestCase {
289
+ name: "ok",
290
+ key: br#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
291
+ value: 256.0,
292
+ buf_len: 256,
293
+ expected_entry: Some(TestEntry {
294
+ header: 61,
295
+ json: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
296
+ padding_len: 7,
297
+ value: 256.0,
298
+ }),
299
+ expected_resp: Ok(72),
300
+ },
301
+ TestCase {
302
+ name: "zero length key",
303
+ key: b"",
304
+ value: 1.0,
305
+ buf_len: 256,
306
+ expected_entry: Some(TestEntry {
307
+ header: 0,
308
+ json: "",
309
+ padding_len: 4,
310
+ value: 1.0,
311
+ }),
312
+ expected_resp: Ok(8),
313
+ },
314
+ TestCase {
315
+ name: "infinite value",
316
+ key: br#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
317
+ value: f64::INFINITY,
318
+ buf_len: 256,
319
+ expected_entry: Some(TestEntry {
320
+ header: 61,
321
+ json: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
322
+ padding_len: 7,
323
+ value: f64::INFINITY,
324
+ }),
325
+ expected_resp: Ok(72),
326
+ },
327
+ TestCase {
328
+ name: "buf len matches entry len",
329
+ key: br#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
330
+ value: 1.0,
331
+ buf_len: 80,
332
+ expected_entry: Some(TestEntry {
333
+ header: 61,
334
+ json: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
335
+ padding_len: 7,
336
+ value: 1.0,
337
+ }),
338
+ expected_resp: Ok(72),
339
+ },
340
+ TestCase {
341
+ name: "buf much too short",
342
+ key: br#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
343
+ value: 1.0,
344
+ buf_len: 5,
345
+ expected_entry: None,
346
+ expected_resp: Err(MmapError::Other(format!(
347
+ "entry length {} larger than slice length {}",
348
+ 80, 5,
349
+ ))),
350
+ },
351
+ TestCase {
352
+ name: "buf short by one",
353
+ key: br#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
354
+ value: 1.0,
355
+ buf_len: 79,
356
+ expected_entry: None,
357
+ expected_resp: Err(MmapError::Other(format!(
358
+ "entry length {} larger than slice length {}",
359
+ 80, 79,
360
+ ))),
361
+ },
362
+ ];
363
+
364
+ for case in tc {
365
+ let mut buf = vec![0; case.buf_len];
366
+ let resp = RawEntry::save(&mut buf, case.key, case.value);
367
+
368
+ assert_eq!(
369
+ case.expected_resp, resp,
370
+ "test case: {} - response",
371
+ case.name,
372
+ );
373
+
374
+ if let Some(e) = case.expected_entry {
375
+ let expected_buf = e.as_bstring();
376
+
377
+ assert_eq!(
378
+ expected_buf,
379
+ buf[..expected_buf.len()].as_bstr(),
380
+ "test case: {} - buffer state",
381
+ case.name
382
+ );
383
+
384
+ let header_len = u32::from_ne_bytes(buf[..size_of::<u32>()].try_into().unwrap());
385
+ assert_eq!(
386
+ case.key.len(),
387
+ header_len as usize,
388
+ "test case: {} - size header",
389
+ case.name,
390
+ );
391
+ }
392
+ }
393
+ }
394
+
395
+ #[test]
396
+ fn test_calc_value_offset() {
397
+ struct TestCase {
398
+ name: &'static str,
399
+ encoded_len: usize,
400
+ expected_value_offset: Option<usize>,
401
+ expected_total_len: Option<usize>,
402
+ expected_err: Option<MmapError>,
403
+ }
404
+
405
+ let tc = vec![
406
+ TestCase {
407
+ name: "ok",
408
+ encoded_len: 8,
409
+ expected_value_offset: Some(16),
410
+ expected_total_len: Some(24),
411
+ expected_err: None,
412
+ },
413
+ TestCase {
414
+ name: "padding length one",
415
+ encoded_len: 3,
416
+ expected_value_offset: Some(8),
417
+ expected_total_len: Some(16),
418
+ expected_err: None,
419
+ },
420
+ TestCase {
421
+ name: "padding length eight",
422
+ encoded_len: 4,
423
+ expected_value_offset: Some(16),
424
+ expected_total_len: Some(24),
425
+ expected_err: None,
426
+ },
427
+ TestCase {
428
+ name: "encoded len gt i32::MAX",
429
+ encoded_len: i32::MAX as usize + 1,
430
+ expected_value_offset: None,
431
+ expected_total_len: None,
432
+ expected_err: Some(MmapError::KeyLength),
433
+ },
434
+ ];
435
+
436
+ for case in tc {
437
+ let name = case.name;
438
+ if let Some(expected_value_offset) = case.expected_value_offset {
439
+ assert_eq!(
440
+ expected_value_offset,
441
+ RawEntry::calc_value_offset(case.encoded_len).unwrap(),
442
+ "test case: {name} - value offset"
443
+ );
444
+ }
445
+
446
+ if let Some(expected_total_len) = case.expected_total_len {
447
+ assert_eq!(
448
+ expected_total_len,
449
+ RawEntry::calc_total_len(case.encoded_len).unwrap(),
450
+ "test case: {name} - total len"
451
+ );
452
+ }
453
+
454
+ if let Some(expected_err) = case.expected_err {
455
+ assert_eq!(
456
+ expected_err,
457
+ RawEntry::calc_value_offset(case.encoded_len).unwrap_err(),
458
+ "test case: {name} - err"
459
+ );
460
+ }
461
+ }
462
+ }
463
+
464
+ #[test]
465
+ fn test_padding_len() {
466
+ for encoded_len in 0..64 {
467
+ let padding = RawEntry::padding_len(encoded_len);
468
+
469
+ // Validate we're actually aligning to 8 bytes.
470
+ assert!((size_of::<u32>() + encoded_len + padding) % 8 == 0)
471
+ }
472
+ }
473
+ }
@@ -0,0 +1,222 @@
1
+ use bstr::{BString, B};
2
+ use std::fs::File;
3
+ use std::io::{Read, Seek, Write};
4
+ use std::path::PathBuf;
5
+ use tempfile::{tempdir, TempDir};
6
+
7
+ use crate::raw_entry::RawEntry;
8
+ use crate::HEADER_SIZE;
9
+
10
+ #[derive(PartialEq, Default, Debug)]
11
+ pub struct TestEntry {
12
+ pub header: u32,
13
+ pub json: &'static str,
14
+ pub padding_len: usize,
15
+ pub value: f64,
16
+ }
17
+
18
+ impl TestEntry {
19
+ pub fn new(json: &'static str, value: f64) -> Self {
20
+ TestEntry {
21
+ header: json.len() as u32,
22
+ json,
23
+ padding_len: RawEntry::padding_len(json.len()),
24
+ value,
25
+ }
26
+ }
27
+
28
+ pub fn as_bytes(&self) -> Vec<u8> {
29
+ [
30
+ B(&self.header.to_ne_bytes()),
31
+ self.json.as_bytes(),
32
+ &vec![b' '; self.padding_len],
33
+ B(&self.value.to_ne_bytes()),
34
+ ]
35
+ .concat()
36
+ }
37
+ pub fn as_bstring(&self) -> BString {
38
+ [
39
+ B(&self.header.to_ne_bytes()),
40
+ self.json.as_bytes(),
41
+ &vec![b' '; self.padding_len],
42
+ B(&self.value.to_ne_bytes()),
43
+ ]
44
+ .concat()
45
+ .into()
46
+ }
47
+
48
+ pub fn as_bytes_no_header(&self) -> BString {
49
+ [
50
+ self.json.as_bytes(),
51
+ &vec![b' '; self.padding_len],
52
+ B(&self.value.to_ne_bytes()),
53
+ ]
54
+ .concat()
55
+ .into()
56
+ }
57
+ }
58
+
59
+ /// Format the data for a `.db` file.
60
+ /// Optional header value can be used to set an invalid `used` size.
61
+ pub fn entries_to_db(entries: &[&'static str], values: &[f64], header: Option<u32>) -> Vec<u8> {
62
+ let mut out = Vec::new();
63
+
64
+ let entry_bytes: Vec<_> = entries
65
+ .iter()
66
+ .zip(values)
67
+ .flat_map(|(e, val)| TestEntry::new(e, *val).as_bytes())
68
+ .collect();
69
+
70
+ let used = match header {
71
+ Some(u) => u,
72
+ None => (entry_bytes.len() + HEADER_SIZE) as u32,
73
+ };
74
+
75
+ out.extend(used.to_ne_bytes());
76
+ out.extend(&[0x0u8; 4]); // Padding.
77
+ out.extend(entry_bytes);
78
+
79
+ out
80
+ }
81
+
82
+ /// A temporary file, path, and dir for use with testing.
83
+ #[derive(Debug)]
84
+ pub struct TestFile {
85
+ pub file: File,
86
+ pub path: PathBuf,
87
+ pub dir: TempDir,
88
+ }
89
+
90
+ impl TestFile {
91
+ pub fn new(file_data: &[u8]) -> TestFile {
92
+ let dir = tempdir().unwrap();
93
+ let path = dir.path().join("test.db");
94
+ let mut file = File::options()
95
+ .create(true)
96
+ .read(true)
97
+ .write(true)
98
+ .open(&path)
99
+ .unwrap();
100
+
101
+ file.write_all(file_data).unwrap();
102
+ file.sync_all().unwrap();
103
+ file.rewind().unwrap();
104
+
105
+ // We need to keep `dir` in scope so it doesn't drop before the files it
106
+ // contains, which may prevent cleanup.
107
+ TestFile { file, path, dir }
108
+ }
109
+ }
110
+
111
+ mod test {
112
+ use super::*;
113
+
114
+ #[test]
115
+ fn test_entry_new() {
116
+ let json = "foobar";
117
+ let value = 1.0f64;
118
+ let expected = TestEntry {
119
+ header: 6,
120
+ json,
121
+ padding_len: 6,
122
+ value,
123
+ };
124
+
125
+ let actual = TestEntry::new(json, value);
126
+ assert_eq!(expected, actual);
127
+ }
128
+
129
+ #[test]
130
+ fn test_entry_bytes() {
131
+ let json = "foobar";
132
+ let value = 1.0f64;
133
+ let expected = [
134
+ &6u32.to_ne_bytes(),
135
+ B(json),
136
+ &[b' '; 6],
137
+ &value.to_ne_bytes(),
138
+ ]
139
+ .concat();
140
+
141
+ let actual = TestEntry::new(json, value).as_bstring();
142
+ assert_eq!(expected, actual);
143
+ }
144
+
145
+ #[test]
146
+ fn test_entry_bytes_no_header() {
147
+ let json = "foobar";
148
+ let value = 1.0f64;
149
+ let expected = [B(json), &[b' '; 6], &value.to_ne_bytes()].concat();
150
+
151
+ let actual = TestEntry::new(json, value).as_bytes_no_header();
152
+ assert_eq!(expected, actual);
153
+ }
154
+
155
+ #[test]
156
+ fn test_entries_to_db_header_correct() {
157
+ let json = &["foobar", "qux"];
158
+ let values = &[1.0, 2.0];
159
+
160
+ let out = entries_to_db(json, values, None);
161
+
162
+ assert_eq!(48u32.to_ne_bytes(), out[0..4], "used set correctly");
163
+ assert_eq!([0u8; 4], out[4..8], "padding set");
164
+ assert_eq!(
165
+ TestEntry::new(json[0], values[0]).as_bytes(),
166
+ out[8..32],
167
+ "first entry matches"
168
+ );
169
+ assert_eq!(
170
+ TestEntry::new(json[1], values[1]).as_bytes(),
171
+ out[32..48],
172
+ "second entry matches"
173
+ );
174
+ }
175
+
176
+ #[test]
177
+ fn test_entries_to_db_header_wrong() {
178
+ let json = &["foobar", "qux"];
179
+ let values = &[1.0, 2.0];
180
+
181
+ const WRONG_USED: u32 = 1000;
182
+ let out = entries_to_db(json, values, Some(WRONG_USED));
183
+
184
+ assert_eq!(
185
+ WRONG_USED.to_ne_bytes(),
186
+ out[0..4],
187
+ "used set to value requested"
188
+ );
189
+ assert_eq!([0u8; 4], out[4..8], "padding set");
190
+ assert_eq!(
191
+ TestEntry::new(json[0], values[0]).as_bytes(),
192
+ out[8..32],
193
+ "first entry matches"
194
+ );
195
+ assert_eq!(
196
+ TestEntry::new(json[1], values[1]).as_bytes(),
197
+ out[32..48],
198
+ "second entry matches"
199
+ );
200
+ }
201
+
202
+ #[test]
203
+ fn test_file() {
204
+ let mut test_file = TestFile::new(b"foobar");
205
+ let stat = test_file.file.metadata().unwrap();
206
+
207
+ assert_eq!(6, stat.len(), "file length");
208
+ assert_eq!(
209
+ 0,
210
+ test_file.file.stream_position().unwrap(),
211
+ "at start of file"
212
+ );
213
+ let mut out_buf = vec![0u8; 256];
214
+ let read_result = test_file.file.read(&mut out_buf);
215
+ assert!(read_result.is_ok());
216
+ assert_eq!(6, read_result.unwrap(), "file is readable");
217
+
218
+ let write_result = test_file.file.write(b"qux");
219
+ assert!(write_result.is_ok());
220
+ assert_eq!(3, write_result.unwrap(), "file is writable");
221
+ }
222
+ }