prometheus-client-mmap 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,458 @@
1
+ use ahash::AHashMap;
2
+ use magnus::{exception::*, Error, RArray};
3
+ use std::mem::size_of;
4
+
5
+ use crate::error::MmapError;
6
+ use crate::file_entry::{EntryData, EntryMetadata, FileEntry};
7
+ use crate::file_info::FileInfo;
8
+ use crate::raw_entry::RawEntry;
9
+ use crate::util::read_u32;
10
+ use crate::Result;
11
+ use crate::{err, HEADER_SIZE};
12
+
13
+ /// A HashMap of JSON strings and their associated metadata.
14
+ /// Used to print metrics in text format.
15
+ ///
16
+ /// The map key is the entry's JSON string and an optional pid string. The latter
17
+ /// allows us to have multiple entries on the map for multiple pids using the
18
+ /// same string.
19
+ #[derive(Default, Debug)]
20
+ pub struct EntryMap(AHashMap<EntryData, EntryMetadata>);
21
+
22
+ impl EntryMap {
23
+ /// Construct a new EntryMap.
24
+ pub fn new() -> Self {
25
+ Self(AHashMap::new())
26
+ }
27
+
28
+ /// Given a list of files, read each one into memory and parse the metrics it contains.
29
+ pub fn aggregate_files(&mut self, list_of_files: RArray) -> magnus::error::Result<()> {
30
+ // Pre-allocate the `HashMap` and validate we don't OOM. The C implementation
31
+ // ignores allocation failures here. We perform this check to avoid potential
32
+ // panics. We assume ~1,000 entries per file, so 72 KiB allocated per file.
33
+ self.0
34
+ .try_reserve(list_of_files.len() * 1024)
35
+ .map_err(|_| {
36
+ err!(
37
+ no_mem_error(),
38
+ "Couldn't allocate for {} memory",
39
+ size_of::<FileEntry>() * list_of_files.len() * 1024
40
+ )
41
+ })?;
42
+
43
+ // We expect file sizes between 4KiB and 4MiB. Pre-allocate 16KiB to reduce reallocations
44
+ // a bit.
45
+ let mut buf = Vec::new();
46
+ buf.try_reserve(16_384)
47
+ .map_err(|_| err!(no_mem_error(), "Couldn't allocate for {} memory", 16_384))?;
48
+
49
+ for item in list_of_files.each() {
50
+ let params = RArray::from_value(item?).expect("file list was not a Ruby Array");
51
+ if params.len() != 4 {
52
+ return Err(err!(
53
+ arg_error(),
54
+ "wrong number of arguments {} instead of 4",
55
+ params.len()
56
+ ));
57
+ }
58
+
59
+ let params = params.to_value_array::<4>()?;
60
+
61
+ let mut file_info = FileInfo::open_from_params(&params)?;
62
+ file_info.read_from_file(&mut buf)?;
63
+ self.process_buffer(file_info, &buf)?;
64
+ }
65
+ Ok(())
66
+ }
67
+
68
+ /// Consume the `EntryMap` and convert the key/value into`FileEntry`
69
+ /// objects, sorting them by their JSON strings.
70
+ pub fn into_sorted(self) -> Result<Vec<FileEntry>> {
71
+ let mut sorted = Vec::new();
72
+
73
+ // To match the behavior of the C version, pre-allocate the entries
74
+ // and check for allocation failure. Generally idiomatic Rust would
75
+ // `collect` the iterator into a new `Vec` in place, but this panics
76
+ // if it can't allocate and we want to continue execution in that
77
+ // scenario.
78
+ if sorted.try_reserve_exact(self.0.len()).is_err() {
79
+ return Err(MmapError::OutOfMemory(
80
+ self.0.len() * size_of::<FileEntry>(),
81
+ ));
82
+ }
83
+
84
+ sorted.extend(
85
+ self.0
86
+ .into_iter()
87
+ .map(|(data, meta)| FileEntry { data, meta }),
88
+ );
89
+
90
+ sorted.sort_unstable_by(|x, y| x.data.cmp(&y.data));
91
+
92
+ Ok(sorted)
93
+ }
94
+
95
+ /// Check if the `EntryMap` already contains the JSON string.
96
+ /// If yes, update the associated value, if not insert the
97
+ /// entry into the map.
98
+ pub fn merge_or_store(&mut self, data: EntryData, meta: EntryMetadata) {
99
+ if let Some(existing) = self.0.get_mut(&data) {
100
+ existing.merge(&meta);
101
+ } else {
102
+ self.0.insert(data, meta);
103
+ }
104
+ }
105
+
106
+ /// Parse metrics data from a `.db` file and store in the `EntryMap`.
107
+ fn process_buffer(&mut self, file_info: FileInfo, source: &[u8]) -> Result<()> {
108
+ if source.len() < HEADER_SIZE {
109
+ // Nothing to read, OK.
110
+ return Ok(());
111
+ }
112
+
113
+ // CAST: no-op on 32-bit, widening on 64-bit.
114
+ let used = read_u32(source, 0)? as usize;
115
+
116
+ if used > source.len() {
117
+ return Err(MmapError::PromParsing(format!(
118
+ "source file {} corrupted, used {used} > file size {}",
119
+ file_info.path.display(),
120
+ source.len()
121
+ )));
122
+ }
123
+
124
+ let mut pos = HEADER_SIZE;
125
+
126
+ while pos + size_of::<u32>() < used {
127
+ let raw_entry = RawEntry::from_slice(&source[pos..used])?;
128
+
129
+ if pos + raw_entry.total_len() > used {
130
+ return Err(MmapError::PromParsing(format!(
131
+ "source file {} corrupted, used {used} < stored data length {}",
132
+ file_info.path.display(),
133
+ pos + raw_entry.total_len()
134
+ )));
135
+ }
136
+
137
+ let meta = EntryMetadata::new(&raw_entry, &file_info)?;
138
+ let data = EntryData::new(&raw_entry, &file_info, meta.is_pid_significant())?;
139
+
140
+ self.merge_or_store(data, meta);
141
+
142
+ pos += raw_entry.total_len();
143
+ }
144
+
145
+ Ok(())
146
+ }
147
+ }
148
+
149
+ #[cfg(test)]
150
+ mod test {
151
+ use magnus::{StaticSymbol, Symbol};
152
+ use std::mem;
153
+
154
+ use super::*;
155
+ use crate::file_entry::FileEntry;
156
+ use crate::testhelper::{self, TestFile};
157
+
158
+ #[test]
159
+ fn test_into_sorted() {
160
+ let _cleanup = unsafe { magnus::embed::init() };
161
+ let ruby = magnus::Ruby::get().unwrap();
162
+ crate::init(&ruby).unwrap();
163
+
164
+ let entries = vec![
165
+ FileEntry {
166
+ data: EntryData {
167
+ json: "zzzzzz".to_string(),
168
+ pid: Some("worker-0_0".to_string()),
169
+ },
170
+ meta: EntryMetadata {
171
+ multiprocess_mode: Symbol::new("max"),
172
+ type_: StaticSymbol::new("gauge"),
173
+ value: 1.0,
174
+ },
175
+ },
176
+ FileEntry {
177
+ data: EntryData {
178
+ json: "zzz".to_string(),
179
+ pid: Some("worker-0_0".to_string()),
180
+ },
181
+ meta: EntryMetadata {
182
+ multiprocess_mode: Symbol::new("max"),
183
+ type_: StaticSymbol::new("gauge"),
184
+ value: 1.0,
185
+ },
186
+ },
187
+ FileEntry {
188
+ data: EntryData {
189
+ json: "zzzaaa".to_string(),
190
+ pid: Some("worker-0_0".to_string()),
191
+ },
192
+ meta: EntryMetadata {
193
+ multiprocess_mode: Symbol::new("max"),
194
+ type_: StaticSymbol::new("gauge"),
195
+ value: 1.0,
196
+ },
197
+ },
198
+ FileEntry {
199
+ data: EntryData {
200
+ json: "aaa".to_string(),
201
+ pid: Some("worker-0_0".to_string()),
202
+ },
203
+ meta: EntryMetadata {
204
+ multiprocess_mode: Symbol::new("max"),
205
+ type_: StaticSymbol::new("gauge"),
206
+ value: 1.0,
207
+ },
208
+ },
209
+ FileEntry {
210
+ data: EntryData {
211
+ json: "ooo".to_string(),
212
+ pid: Some("worker-1_0".to_string()),
213
+ },
214
+ meta: EntryMetadata {
215
+ multiprocess_mode: Symbol::new("all"),
216
+ type_: StaticSymbol::new("gauge"),
217
+ value: 1.0,
218
+ },
219
+ },
220
+ FileEntry {
221
+ data: EntryData {
222
+ json: "ooo".to_string(),
223
+ pid: Some("worker-0_0".to_string()),
224
+ },
225
+ meta: EntryMetadata {
226
+ multiprocess_mode: Symbol::new("all"),
227
+ type_: StaticSymbol::new("gauge"),
228
+ value: 1.0,
229
+ },
230
+ },
231
+ ];
232
+
233
+ let mut map = EntryMap::new();
234
+
235
+ for entry in entries {
236
+ map.0.insert(entry.data, entry.meta);
237
+ }
238
+
239
+ let result = map.into_sorted();
240
+ assert!(result.is_ok());
241
+ let sorted = result.unwrap();
242
+ assert_eq!(sorted.len(), 6);
243
+ assert_eq!(sorted[0].data.json, "aaa");
244
+ assert_eq!(sorted[1].data.json, "ooo");
245
+ assert_eq!(sorted[1].data.pid.as_deref(), Some("worker-0_0"));
246
+ assert_eq!(sorted[2].data.json, "ooo");
247
+ assert_eq!(sorted[2].data.pid.as_deref(), Some("worker-1_0"));
248
+ assert_eq!(sorted[3].data.json, "zzz");
249
+ assert_eq!(sorted[4].data.json, "zzzaaa");
250
+ assert_eq!(sorted[5].data.json, "zzzzzz");
251
+ }
252
+
253
+ #[test]
254
+ fn test_merge_or_store() {
255
+ let _cleanup = unsafe { magnus::embed::init() };
256
+ let ruby = magnus::Ruby::get().unwrap();
257
+ crate::init(&ruby).unwrap();
258
+
259
+ let key = "foobar";
260
+
261
+ let starting_entry = FileEntry {
262
+ data: EntryData {
263
+ json: key.to_string(),
264
+ pid: Some("worker-0_0".to_string()),
265
+ },
266
+ meta: EntryMetadata {
267
+ multiprocess_mode: Symbol::new("all"),
268
+ type_: StaticSymbol::new("gauge"),
269
+ value: 1.0,
270
+ },
271
+ };
272
+
273
+ let matching_entry = FileEntry {
274
+ data: EntryData {
275
+ json: key.to_string(),
276
+ pid: Some("worker-0_0".to_string()),
277
+ },
278
+ meta: EntryMetadata {
279
+ multiprocess_mode: Symbol::new("all"),
280
+ type_: StaticSymbol::new("gauge"),
281
+ value: 5.0,
282
+ },
283
+ };
284
+
285
+ let same_key_different_worker = FileEntry {
286
+ data: EntryData {
287
+ json: key.to_string(),
288
+ pid: Some("worker-1_0".to_string()),
289
+ },
290
+ meta: EntryMetadata {
291
+ multiprocess_mode: Symbol::new("all"),
292
+ type_: StaticSymbol::new("gauge"),
293
+ value: 100.0,
294
+ },
295
+ };
296
+
297
+ let unmatched_entry = FileEntry {
298
+ data: EntryData {
299
+ json: "another key".to_string(),
300
+ pid: Some("worker-0_0".to_string()),
301
+ },
302
+ meta: EntryMetadata {
303
+ multiprocess_mode: Symbol::new("all"),
304
+ type_: StaticSymbol::new("gauge"),
305
+ value: 1.0,
306
+ },
307
+ };
308
+
309
+ let mut map = EntryMap::new();
310
+
311
+ map.0
312
+ .insert(starting_entry.data.clone(), starting_entry.meta.clone());
313
+
314
+ map.merge_or_store(matching_entry.data, matching_entry.meta);
315
+
316
+ assert_eq!(
317
+ 5.0,
318
+ map.0.get(&starting_entry.data).unwrap().value,
319
+ "value updated"
320
+ );
321
+ assert_eq!(1, map.0.len(), "no entry added");
322
+
323
+ map.merge_or_store(
324
+ same_key_different_worker.data,
325
+ same_key_different_worker.meta,
326
+ );
327
+
328
+ assert_eq!(
329
+ 5.0,
330
+ map.0.get(&starting_entry.data).unwrap().value,
331
+ "value unchanged"
332
+ );
333
+
334
+ assert_eq!(2, map.0.len(), "additional entry added");
335
+
336
+ map.merge_or_store(unmatched_entry.data, unmatched_entry.meta);
337
+
338
+ assert_eq!(
339
+ 5.0,
340
+ map.0.get(&starting_entry.data).unwrap().value,
341
+ "value unchanged"
342
+ );
343
+ assert_eq!(3, map.0.len(), "entry added");
344
+ }
345
+
346
+ #[test]
347
+ fn test_process_buffer() {
348
+ struct TestCase {
349
+ name: &'static str,
350
+ json: &'static [&'static str],
351
+ values: &'static [f64],
352
+ used: Option<u32>,
353
+ expected_ct: usize,
354
+ expected_err: Option<MmapError>,
355
+ }
356
+
357
+ let _cleanup = unsafe { magnus::embed::init() };
358
+ let ruby = magnus::Ruby::get().unwrap();
359
+ crate::init(&ruby).unwrap();
360
+
361
+ let tc = vec![
362
+ TestCase {
363
+ name: "single entry",
364
+ json: &[
365
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
366
+ ],
367
+ values: &[1.0],
368
+ used: None,
369
+ expected_ct: 1,
370
+ expected_err: None,
371
+ },
372
+ TestCase {
373
+ name: "multiple entries",
374
+ json: &[
375
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
376
+ r#"["second_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
377
+ ],
378
+ values: &[1.0, 2.0],
379
+ used: None,
380
+ expected_ct: 2,
381
+ expected_err: None,
382
+ },
383
+ TestCase {
384
+ name: "empty",
385
+ json: &[],
386
+ values: &[],
387
+ used: None,
388
+ expected_ct: 0,
389
+ expected_err: None,
390
+ },
391
+ TestCase {
392
+ name: "used too long",
393
+ json: &[
394
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
395
+ ],
396
+ values: &[1.0],
397
+ used: Some(9999),
398
+ expected_ct: 0,
399
+ expected_err: Some(MmapError::PromParsing(String::new())),
400
+ },
401
+ TestCase {
402
+ name: "used too short",
403
+ json: &[
404
+ r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
405
+ ],
406
+ values: &[1.0],
407
+ used: Some(15),
408
+ expected_ct: 0,
409
+ expected_err: Some(MmapError::out_of_bounds(88, 7)),
410
+ },
411
+ ];
412
+
413
+ for case in tc {
414
+ let name = case.name;
415
+
416
+ let input_bytes = testhelper::entries_to_db(case.json, case.values, case.used);
417
+
418
+ let TestFile {
419
+ file,
420
+ path,
421
+ dir: _dir,
422
+ } = TestFile::new(&input_bytes);
423
+
424
+ let info = FileInfo {
425
+ file,
426
+ path,
427
+ len: case.json.len(),
428
+ multiprocess_mode: Symbol::new("max"),
429
+ type_: StaticSymbol::new("gauge"),
430
+ pid: "worker-1".to_string(),
431
+ };
432
+
433
+ let mut map = EntryMap::new();
434
+ let result = map.process_buffer(info, &input_bytes);
435
+
436
+ assert_eq!(case.expected_ct, map.0.len(), "test case: {name} - count");
437
+
438
+ if let Some(expected_err) = case.expected_err {
439
+ // Validate we have the right enum type for the error. Error
440
+ // messages contain the temp dir path and can't be predicted
441
+ // exactly.
442
+ assert_eq!(
443
+ mem::discriminant(&expected_err),
444
+ mem::discriminant(&result.unwrap_err()),
445
+ "test case: {name} - failure"
446
+ );
447
+ } else {
448
+ assert_eq!(Ok(()), result, "test case: {name} - success");
449
+
450
+ assert_eq!(
451
+ case.json.len(),
452
+ map.0.len(),
453
+ "test case: {name} - all entries captured"
454
+ );
455
+ }
456
+ }
457
+ }
458
+ }
@@ -0,0 +1,151 @@
1
+ use magnus::typed_data::Obj;
2
+ use magnus::value::Fixnum;
3
+ use magnus::{Integer, RArray, RClass, RHash, RString, Value};
4
+
5
+ use crate::file_entry::FileEntry;
6
+ use crate::map::EntryMap;
7
+
8
+ /// A Rust struct wrapped in a Ruby object, providing access to a memory-mapped
9
+ /// file used to store, update, and read out Prometheus metrics.
10
+ ///
11
+ /// - File format:
12
+ /// - Header:
13
+ /// - 4 bytes: u32 - total size of metrics in file.
14
+ /// - 4 bytes: NUL byte padding.
15
+ /// - Repeating metrics entries:
16
+ /// - 4 bytes: u32 - entry JSON string size.
17
+ /// - `N` bytes: UTF-8 encoded JSON string used as entry key.
18
+ /// - (8 - (4 + `N`) % 8) bytes: 1 to 8 padding space (0x20) bytes to
19
+ /// reach 8-byte alignment.
20
+ /// - 8 bytes: f64 - entry value.
21
+ ///
22
+ /// All numbers are saved in native-endian format.
23
+ ///
24
+ /// Generated via [luismartingarcia/protocol](https://github.com/luismartingarcia/protocol):
25
+ ///
26
+ ///
27
+ /// ```
28
+ /// protocol "Used:4,Pad:4,K1 Size:4,K1 Name:4,K1 Value:8,K2 Size:4,K2 Name:4,K2 Value:8"
29
+ ///
30
+ /// 0 1 2 3
31
+ /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
32
+ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
33
+ /// | Used | Pad |K1 Size|K1 Name| K1 Value |K2 Size|K2 Name|
34
+ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
35
+ /// | K2 Value |
36
+ /// +-+-+-+-+-+-+-+
37
+ /// ```
38
+ #[derive(Debug, Default)]
39
+ #[magnus::wrap(class = "FastMmapedFileRs", free_immediately, size)]
40
+ pub struct MmapedFile;
41
+
42
+ impl MmapedFile {
43
+ /// call-seq:
44
+ /// new(file)
45
+ ///
46
+ /// create a new Mmap object
47
+ ///
48
+ /// * <em>file</em>
49
+ ///
50
+ ///
51
+ /// Creates a mapping that's shared with all other processes
52
+ /// mapping the same area of the file.
53
+ pub fn new(_klass: RClass, _args: &[Value]) -> magnus::error::Result<Obj<Self>> {
54
+ Ok(Obj::wrap(Self))
55
+ }
56
+
57
+ /// Initialize a new `FastMmapedFileRs` object. This must be defined in
58
+ /// order for inheritance to work.
59
+ pub fn initialize(_rb_self: Obj<Self>, _fname: String) -> magnus::error::Result<()> {
60
+ unimplemented!();
61
+ }
62
+
63
+ /// Read the list of files provided from Ruby and convert them to a Prometheus
64
+ /// metrics String.
65
+ pub fn to_metrics(file_list: RArray) -> magnus::error::Result<String> {
66
+ let mut map = EntryMap::new();
67
+ map.aggregate_files(file_list)?;
68
+
69
+ let sorted = map.into_sorted()?;
70
+
71
+ FileEntry::entries_to_string(sorted).map_err(|e| e.into())
72
+ }
73
+
74
+ /// Document-method: []
75
+ /// Document-method: slice
76
+ ///
77
+ /// call-seq: [](args)
78
+ ///
79
+ /// Element reference - with the following syntax:
80
+ ///
81
+ /// self[nth]
82
+ ///
83
+ /// retrieve the <em>nth</em> character
84
+ ///
85
+ /// self[start..last]
86
+ ///
87
+ /// return a substring from <em>start</em> to <em>last</em>
88
+ ///
89
+ /// self[start, length]
90
+ ///
91
+ /// return a substring of <em>lenght</em> characters from <em>start</em>
92
+ pub fn slice(_rb_self: Obj<Self>, _args: &[Value]) -> magnus::error::Result<RString> {
93
+ unimplemented!();
94
+ }
95
+
96
+ /// Document-method: msync
97
+ /// Document-method: sync
98
+ /// Document-method: flush
99
+ ///
100
+ /// call-seq: msync
101
+ ///
102
+ /// flush the file
103
+ pub fn sync(&self, _args: &[Value]) -> magnus::error::Result<()> {
104
+ unimplemented!();
105
+ }
106
+
107
+ /// Document-method: munmap
108
+ /// Document-method: unmap
109
+ ///
110
+ /// call-seq: munmap
111
+ ///
112
+ /// terminate the association
113
+ pub fn munmap(_rb_self: Obj<Self>) -> magnus::error::Result<()> {
114
+ unimplemented!();
115
+ }
116
+
117
+ /// Fetch the `used` header from the `.db` file, the length
118
+ /// in bytes of the data written to the file.
119
+ pub fn load_used(&self) -> magnus::error::Result<Integer> {
120
+ unimplemented!();
121
+ }
122
+
123
+ /// Update the `used` header for the `.db` file, the length
124
+ /// in bytes of the data written to the file.
125
+ pub fn save_used(_rb_self: Obj<Self>, _used: Fixnum) -> magnus::error::Result<Fixnum> {
126
+ unimplemented!();
127
+ }
128
+
129
+ /// Fetch the value associated with a key from the mmap.
130
+ /// If no entry is present, initialize with the default
131
+ /// value provided.
132
+ pub fn fetch_entry(
133
+ _rb_self: Obj<Self>,
134
+ _positions: RHash,
135
+ _key: RString,
136
+ _default_value: f64,
137
+ ) -> magnus::error::Result<f64> {
138
+ unimplemented!();
139
+ }
140
+
141
+ /// Update the value of an existing entry, if present. Otherwise create a new entry
142
+ /// for the key.
143
+ pub fn upsert_entry(
144
+ _rb_self: Obj<Self>,
145
+ _positions: RHash,
146
+ _key: RString,
147
+ _value: f64,
148
+ ) -> magnus::error::Result<f64> {
149
+ unimplemented!();
150
+ }
151
+ }