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,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
+ }