prometheus-client-mmap 0.23.1 → 0.24.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3125bbd60563b936ffa17d09e8e580e8120d0e3b55d9cc1a88c8bbcdce1e3f89
4
- data.tar.gz: e682ce4a96d84a7ca4587853044574e93df02003d71714179b0777a555656eb6
3
+ metadata.gz: 6db16be87c209d7fa1ff15a76494ea04d1329a91de10aa1d5d53c6436bdcf557
4
+ data.tar.gz: cedd91b3c15aeb475e1be62e1514374a0c59cea2a502ab465dba5492a4b69b2f
5
5
  SHA512:
6
- metadata.gz: 75c5deb43e465930c58eb0c494df46ffb81674bd8cb2540b5cd99f4ad87a0eb4bd5daeadbc5277b93d8455c2613b45c03f7f916db52c206727040f1f6ef35129
7
- data.tar.gz: b7d404084d86a1795d52757683de4c35ece4241e5c847b3e2939cdf91020e114337e37bad85775761f7303b199e0a2df62d854b0e0a5b8476d68dc0b736d0490
6
+ metadata.gz: dddcc6094038212397c89061b2bacb51830f59e67832d45d0b96ff949f5b3725be91f18c507150b8237ff4a5fca498d7f7ee62a8b55addf2cc4888e62ec114b3
7
+ data.tar.gz: ba8d5ccb8e34a32c57f71321618450287ac9c2ab0c6a3b49efe819049ce7e37b059c3db54e04875d444cd85a42714fab293b4ba57914c1dfdaf4c29255159057
data/README.md CHANGED
@@ -200,6 +200,17 @@ Set `prometheus_multiproc_dir` environment variable to the path where you want m
200
200
  prometheus_multiproc_dir=/tmp
201
201
  ```
202
202
 
203
+ ### Multiprocess metrics via Rust extension
204
+
205
+ If the environment variable `prometheus_rust_multiprocess_metrics=true` is set or if the `rust_multiprocess_metrics`
206
+ configuration setting is `true` and the `fast_mmaped_file_rs` extension is available, it will be used to generate
207
+ multiprocess metrics. This should be significantly faster than the C extension.
208
+
209
+ ### Read and write metrics via Rust extension
210
+
211
+ If the environment variable `prometheus_rust_mmaped_file=true` is set then if the `fast_mmaped_file_rs`
212
+ extension is available it will be used to read and write metrics from the mmapped file.
213
+
203
214
  ## Pitfalls
204
215
 
205
216
  ### PID cardinality
@@ -9,7 +9,7 @@ use crate::util;
9
9
  use crate::PROM_EPARSING_ERROR;
10
10
 
11
11
  /// A lightweight representation of Ruby ExceptionClasses.
12
- #[derive(PartialEq, Clone, Copy, Debug)]
12
+ #[derive(PartialEq, Eq, Clone, Copy, Debug)]
13
13
  pub enum RubyError {
14
14
  Arg,
15
15
  Encoding,
@@ -45,7 +45,7 @@ impl From<RubyError> for magnus::ExceptionClass {
45
45
  /// Errors returned internally within the crate. Methods called directly by Ruby return
46
46
  /// `magnus::error::Error` as do functions that interact heavily with Ruby. This can be
47
47
  /// converted into a `magnus::error::Error` at the boundary between Rust and Ruby.
48
- #[derive(PartialEq, Error, Debug)]
48
+ #[derive(PartialEq, Eq, Error, Debug)]
49
49
  pub enum MmapError {
50
50
  /// A read or write was made while another thread had mutable access to the mmap.
51
51
  #[error("read/write operation attempted while mmap was being written to")]
@@ -1,4 +1,4 @@
1
- use magnus::{StaticSymbol, Symbol};
1
+ use magnus::Symbol;
2
2
  use std::fmt::Write;
3
3
  use std::str;
4
4
 
@@ -80,7 +80,7 @@ impl<'a> BorrowedData<'a> {
80
80
  #[derive(Clone, Debug)]
81
81
  pub struct EntryMetadata {
82
82
  pub multiprocess_mode: Symbol,
83
- pub type_: StaticSymbol,
83
+ pub type_: Symbol,
84
84
  pub value: f64,
85
85
  }
86
86
 
@@ -176,7 +176,7 @@ impl FileEntry {
176
176
  out.push_str(family_name);
177
177
  out.push(' ');
178
178
 
179
- out.push_str(self.meta.type_.name().expect("name was invalid UTF-8"));
179
+ out.push_str(&self.meta.type_.name().expect("name was invalid UTF-8"));
180
180
  out.push('\n');
181
181
  }
182
182
 
@@ -438,7 +438,7 @@ mod test {
438
438
  path,
439
439
  len: case.json.len(),
440
440
  multiprocess_mode: Symbol::new(case.multiprocess_mode),
441
- type_: StaticSymbol::new("gauge"),
441
+ type_: Symbol::new("gauge"),
442
442
  pid: pid.to_string(),
443
443
  };
444
444
  file_infos.push(info);
@@ -544,7 +544,7 @@ mod test {
544
544
  path,
545
545
  len: json.len(),
546
546
  multiprocess_mode: Symbol::new(case.multiprocess_mode),
547
- type_: StaticSymbol::new(case.metric_type),
547
+ type_: Symbol::new(case.metric_type),
548
548
  pid: "worker-1".to_string(),
549
549
  };
550
550
 
@@ -1,5 +1,5 @@
1
1
  use magnus::exception::*;
2
- use magnus::{Error, RString, StaticSymbol, Symbol, Value};
2
+ use magnus::{Error, RString, Symbol, Value};
3
3
  use std::ffi::OsString;
4
4
  use std::fs::File;
5
5
  use std::io::{self, Read, Seek};
@@ -18,7 +18,7 @@ pub struct FileInfo {
18
18
  pub path: PathBuf,
19
19
  pub len: usize,
20
20
  pub multiprocess_mode: Symbol,
21
- pub type_: StaticSymbol,
21
+ pub type_: Symbol,
22
22
  pub pid: String,
23
23
  }
24
24
 
@@ -59,7 +59,7 @@ impl FileInfo {
59
59
  let multiprocess_mode = Symbol::from_value(params[1])
60
60
  .ok_or_else(|| err!(arg_error(), "expected multiprocess_mode to be a symbol"))?;
61
61
 
62
- let type_ = StaticSymbol::from_value(params[2])
62
+ let type_ = Symbol::from_value(params[2])
63
63
  .ok_or_else(|| err!(arg_error(), "expected file type to be a symbol"))?;
64
64
 
65
65
  let pid = RString::from_value(params[3])
@@ -103,7 +103,7 @@ impl FileInfo {
103
103
 
104
104
  #[cfg(test)]
105
105
  mod test {
106
- use magnus::{eval, RArray, StaticSymbol, Symbol};
106
+ use magnus::{eval, RArray, Symbol};
107
107
  use rand::{thread_rng, Rng};
108
108
  use sha2::{Digest, Sha256};
109
109
 
@@ -168,7 +168,7 @@ mod test {
168
168
  path,
169
169
  len: buf.len(),
170
170
  multiprocess_mode: Symbol::new("puma"),
171
- type_: StaticSymbol::new("max"),
171
+ type_: Symbol::new("max"),
172
172
  pid: "worker-0_0".to_string(),
173
173
  };
174
174
 
@@ -166,7 +166,7 @@ impl EntryMap {
166
166
 
167
167
  #[cfg(test)]
168
168
  mod test {
169
- use magnus::{StaticSymbol, Symbol};
169
+ use magnus::Symbol;
170
170
  use std::mem;
171
171
 
172
172
  use super::*;
@@ -197,7 +197,7 @@ mod test {
197
197
  },
198
198
  meta: EntryMetadata {
199
199
  multiprocess_mode: Symbol::new("max"),
200
- type_: StaticSymbol::new("gauge"),
200
+ type_: Symbol::new("gauge"),
201
201
  value: 1.0,
202
202
  },
203
203
  },
@@ -208,7 +208,7 @@ mod test {
208
208
  },
209
209
  meta: EntryMetadata {
210
210
  multiprocess_mode: Symbol::new("max"),
211
- type_: StaticSymbol::new("gauge"),
211
+ type_: Symbol::new("gauge"),
212
212
  value: 1.0,
213
213
  },
214
214
  },
@@ -219,7 +219,7 @@ mod test {
219
219
  },
220
220
  meta: EntryMetadata {
221
221
  multiprocess_mode: Symbol::new("max"),
222
- type_: StaticSymbol::new("gauge"),
222
+ type_: Symbol::new("gauge"),
223
223
  value: 1.0,
224
224
  },
225
225
  },
@@ -230,7 +230,7 @@ mod test {
230
230
  },
231
231
  meta: EntryMetadata {
232
232
  multiprocess_mode: Symbol::new("max"),
233
- type_: StaticSymbol::new("gauge"),
233
+ type_: Symbol::new("gauge"),
234
234
  value: 1.0,
235
235
  },
236
236
  },
@@ -241,7 +241,7 @@ mod test {
241
241
  },
242
242
  meta: EntryMetadata {
243
243
  multiprocess_mode: Symbol::new("all"),
244
- type_: StaticSymbol::new("gauge"),
244
+ type_: Symbol::new("gauge"),
245
245
  value: 1.0,
246
246
  },
247
247
  },
@@ -252,7 +252,7 @@ mod test {
252
252
  },
253
253
  meta: EntryMetadata {
254
254
  multiprocess_mode: Symbol::new("all"),
255
- type_: StaticSymbol::new("gauge"),
255
+ type_: Symbol::new("gauge"),
256
256
  value: 1.0,
257
257
  },
258
258
  },
@@ -293,7 +293,7 @@ mod test {
293
293
  },
294
294
  meta: EntryMetadata {
295
295
  multiprocess_mode: Symbol::new("all"),
296
- type_: StaticSymbol::new("gauge"),
296
+ type_: Symbol::new("gauge"),
297
297
  value: 1.0,
298
298
  },
299
299
  };
@@ -305,7 +305,7 @@ mod test {
305
305
  },
306
306
  meta: EntryMetadata {
307
307
  multiprocess_mode: Symbol::new("all"),
308
- type_: StaticSymbol::new("gauge"),
308
+ type_: Symbol::new("gauge"),
309
309
  value: 5.0,
310
310
  },
311
311
  };
@@ -317,7 +317,7 @@ mod test {
317
317
  },
318
318
  meta: EntryMetadata {
319
319
  multiprocess_mode: Symbol::new("all"),
320
- type_: StaticSymbol::new("gauge"),
320
+ type_: Symbol::new("gauge"),
321
321
  value: 100.0,
322
322
  },
323
323
  };
@@ -329,7 +329,7 @@ mod test {
329
329
  },
330
330
  meta: EntryMetadata {
331
331
  multiprocess_mode: Symbol::new("all"),
332
- type_: StaticSymbol::new("gauge"),
332
+ type_: Symbol::new("gauge"),
333
333
  value: 1.0,
334
334
  },
335
335
  };
@@ -460,7 +460,7 @@ mod test {
460
460
  path,
461
461
  len: case.json.len(),
462
462
  multiprocess_mode: Symbol::new("max"),
463
- type_: StaticSymbol::new("gauge"),
463
+ type_: Symbol::new("gauge"),
464
464
  pid: "worker-1".to_string(),
465
465
  };
466
466
 
@@ -204,6 +204,12 @@ impl InnerMmap {
204
204
  self.map.as_ptr()
205
205
  }
206
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
+
207
213
  /// Perform an msync(2) on the mmap, flushing all changes written
208
214
  /// to disk. The sync may optionally be performed asynchronously.
209
215
  pub fn flush(&mut self, f_async: bool) -> Result<()> {
@@ -3,8 +3,8 @@ use magnus::prelude::*;
3
3
  use magnus::rb_sys::{AsRawValue, FromRawValue};
4
4
  use magnus::typed_data::Obj;
5
5
  use magnus::value::Fixnum;
6
- use magnus::{block::Proc, eval, scan_args, Error, Integer, RArray, RClass, RHash, RString, Value};
7
- use nix::libc::c_long;
6
+ use magnus::{eval, scan_args, Error, Integer, RArray, RClass, RHash, RString, Value};
7
+ use nix::libc::{c_char, c_long, c_ulong};
8
8
  use rb_sys::rb_str_new_static;
9
9
  use std::fs::File;
10
10
  use std::io::{prelude::*, SeekFrom};
@@ -25,6 +25,11 @@ use inner::InnerMmap;
25
25
 
26
26
  mod inner;
27
27
 
28
+ /// The Ruby `STR_NOEMBED` flag, aka `FL_USER1`.
29
+ const STR_NOEMBED: c_ulong = 1 << (13);
30
+ /// The Ruby `STR_SHARED` flag, aka `FL_USER2`.
31
+ const STR_SHARED: c_ulong = 1 << (14);
32
+
28
33
  /// A Rust struct wrapped in a Ruby object, providing access to a memory-mapped
29
34
  /// file used to store, update, and read out Prometheus metrics.
30
35
  ///
@@ -151,13 +156,28 @@ impl MmapedFile {
151
156
  ///
152
157
  /// return a substring of <em>lenght</em> characters from <em>start</em>
153
158
  pub fn slice(rb_self: Obj<Self>, args: &[Value]) -> magnus::error::Result<RString> {
154
- // The C implemenation would trigger a GC cycle via `rb_gc_force_recycle`
159
+ // The C implementation would trigger a GC cycle via `rb_gc_force_recycle`
155
160
  // if the `MM_PROTECT` flag is set, but in practice this is never used.
156
161
  // We omit this logic, particularly because `rb_gc_force_recycle` is a
157
162
  // no-op as of Ruby 3.1.
163
+ let rs_self = &*rb_self;
164
+
165
+ let str = rs_self.str(rb_self)?;
166
+ rs_self._slice(rb_self, str, args)
167
+ }
158
168
 
159
- let str = (*rb_self).str(rb_self)?;
160
- let res = str.funcall("[]", args);
169
+ fn _slice(
170
+ &self,
171
+ rb_self: Obj<Self>,
172
+ str: RString,
173
+ args: &[Value],
174
+ ) -> magnus::error::Result<RString> {
175
+ let substr: RString = str.funcall("[]", args)?;
176
+
177
+ // Track shared child strings which use the same backing storage.
178
+ if Self::rb_string_is_shared(substr) {
179
+ (*rb_self).track_rstring(rb_self, substr)?;
180
+ }
161
181
 
162
182
  // The C implementation does this, perhaps to validate that the len we
163
183
  // provided is actually being used.
@@ -166,7 +186,7 @@ impl MmapedFile {
166
186
  Ok(())
167
187
  })?;
168
188
 
169
- res
189
+ Ok(substr)
170
190
  }
171
191
 
172
192
  /// Document-method: msync
@@ -223,7 +243,8 @@ impl MmapedFile {
223
243
  })?;
224
244
 
225
245
  // Update each String object to be zero-length.
226
- rs_self.update_weak_map(rb_self)?;
246
+ let cap = util::cast_chk::<_, c_long>(rs_self.capacity(), "capacity")?;
247
+ rs_self.update_weak_map(rb_self, rs_self.as_mut_ptr(), cap)?;
227
248
 
228
249
  // Remove the `InnerMmap` from the `RwLock`. This will drop
229
250
  // end of this function, unmapping and closing the file.
@@ -341,16 +362,19 @@ impl MmapedFile {
341
362
  Ok(unsafe { rb_str_new_static(ptr as _, len as _) })
342
363
  })?;
343
364
 
365
+ // SAFETY: We know that rb_str_new_static returns a VALUE.
344
366
  let val = unsafe { Value::from_raw(val_id) };
345
367
 
346
368
  // UNWRAP: We created this value as a string above.
347
369
  let str = RString::from_value(val).unwrap();
348
370
 
349
- let tracker: Value = rb_self.ivar_get("@weak_obj_tracker")?;
371
+ // Freeze the root string so it can't be mutated out from under any
372
+ // substrings created. This object is never exposed to callers.
373
+ str.freeze();
350
374
 
351
- // Use the string's Id as the key in the `WeakMap`.
352
- let key = str.as_raw();
353
- let _: Value = tracker.funcall("[]=", (key, str))?;
375
+ // Track the RString in our `WeakMap` so we can update its address if
376
+ // we re-mmap the backing file.
377
+ (*rb_self).track_rstring(rb_self, str)?;
354
378
 
355
379
  Ok(str)
356
380
  }
@@ -358,23 +382,20 @@ impl MmapedFile {
358
382
  /// If we reallocate, any live Ruby strings provided by the `str()` method
359
383
  /// will be invalidated. We need to iterate over them using and update their
360
384
  /// heap pointers to the newly allocated memory region.
361
- fn update_weak_map(&self, rb_self: Obj<Self>) -> magnus::error::Result<()> {
385
+ fn update_weak_map(
386
+ &self,
387
+ rb_self: Obj<Self>,
388
+ old_ptr: *const c_char,
389
+ old_cap: c_long,
390
+ ) -> magnus::error::Result<()> {
362
391
  let tracker: Value = rb_self.ivar_get("@weak_obj_tracker")?;
363
392
 
364
- let (new_ptr, new_len) = (*rb_self).inner(|inner| {
365
- // Pointers are not `Send`, convert it to usize to allow capture in closure.
366
- let new_ptr = inner.as_ptr() as usize;
367
- let new_len = util::cast_chk::<_, c_long>(inner.len(), "mmap len")?;
393
+ let new_len = self.inner(|inner| util::cast_chk::<_, c_long>(inner.len(), "mmap len"))?;
368
394
 
369
- Ok((new_ptr, new_len))
370
- })?;
371
-
372
- // Allocate a block with a closure that updates the string details.
373
- let block = Proc::from_fn(move |args, _block| {
374
- let val = args
375
- .get(0)
376
- .ok_or_else(|| err!(arg_error(), "no argument received from `each_value`"))?;
377
- let str = RString::from_value(*val)
395
+ // Iterate over the values of the `WeakMap`.
396
+ for val in tracker.enumeratorize("each_value", ()) {
397
+ let rb_string = val?;
398
+ let str = RString::from_value(rb_string)
378
399
  .ok_or_else(|| err!(arg_error(), "weakmap value was not a string"))?;
379
400
 
380
401
  // SAFETY: We're messing with Ruby's internals here, YOLO.
@@ -383,18 +404,42 @@ impl MmapedFile {
383
404
  // which provides access to its internals.
384
405
  let mut raw_str = Self::rb_string_internal(str);
385
406
 
407
+ // Shared string have their own `ptr` and `len` values, but `aux`
408
+ // is the id of the parent string so the GC can track this
409
+ // dependency. The `ptr` will always be an offset from the base
410
+ // address of the mmap, and `len` will be the length of the mmap
411
+ // less the offset from the base.
412
+ if Self::rb_string_is_shared(str) && new_len > 0 {
413
+ // Calculate how far into the original mmap the shared string
414
+ // started and update to the equivalent address in the new
415
+ // one.
416
+ let substr_ptr = raw_str.as_ref().as_.heap.ptr;
417
+ let offset = substr_ptr.offset_from(old_ptr);
418
+
419
+ raw_str.as_mut().as_.heap.ptr = self.as_mut_ptr().offset(offset);
420
+
421
+ let current_len = raw_str.as_ref().as_.heap.len;
422
+ let new_shared_len = old_cap + current_len;
423
+
424
+ raw_str.as_mut().as_.heap.len = new_shared_len;
425
+ continue;
426
+ }
427
+
386
428
  // Update the string to point to the new mmapped file.
387
429
  // We're matching the behavior of Ruby's `str_new_static` function.
388
430
  // See https://github.com/ruby/ruby/blob/e51014f9c05aa65cbf203442d37fef7c12390015/string.c#L1030-L1053
389
- raw_str.as_mut().as_.heap.ptr = new_ptr as _;
431
+ //
432
+ // We deliberately do _NOT_ increment the `capa` field of the
433
+ // string to match the new `len`. We were initially doing this,
434
+ // but consistently triggered GCs in the middle of updating the
435
+ // string pointers, causing a segfault.
436
+ //
437
+ // See https://gitlab.com/gitlab-org/ruby/gems/prometheus-client-mmap/-/issues/45
438
+ raw_str.as_mut().as_.heap.ptr = self.as_mut_ptr();
390
439
  raw_str.as_mut().as_.heap.len = new_len;
391
- raw_str.as_mut().as_.heap.aux.capa = new_len;
392
440
  }
393
- Ok(())
394
- });
441
+ }
395
442
 
396
- // Execute the block.
397
- let _: Value = tracker.funcall_with_block("each_value", (), block)?;
398
443
  Ok(())
399
444
  }
400
445
 
@@ -429,6 +474,9 @@ impl MmapedFile {
429
474
  }
430
475
 
431
476
  if new_cap != self.capacity() {
477
+ let old_ptr = self.as_mut_ptr();
478
+ let old_cap = util::cast_chk::<_, c_long>(self.capacity(), "capacity")?;
479
+
432
480
  // Drop the old mmap.
433
481
  let (mut file, path) = self.take_inner()?.munmap();
434
482
 
@@ -439,7 +487,7 @@ impl MmapedFile {
439
487
 
440
488
  self.insert_inner(new_inner)?;
441
489
 
442
- return self.update_weak_map(rb_self);
490
+ return self.update_weak_map(rb_self, old_ptr, old_cap);
443
491
  }
444
492
 
445
493
  Ok(())
@@ -476,6 +524,15 @@ impl MmapedFile {
476
524
  Ok(())
477
525
  }
478
526
 
527
+ fn track_rstring(&self, rb_self: Obj<Self>, str: RString) -> magnus::error::Result<()> {
528
+ let tracker: Value = rb_self.ivar_get("@weak_obj_tracker")?;
529
+
530
+ // Use the string's Id as the key in the `WeakMap`.
531
+ let key = str.as_raw();
532
+ let _: Value = tracker.funcall("[]=", (key, str))?;
533
+ Ok(())
534
+ }
535
+
479
536
  /// The total capacity of the underlying mmap.
480
537
  #[inline]
481
538
  fn capacity(&self) -> usize {
@@ -489,6 +546,13 @@ impl MmapedFile {
489
546
  .map_err(|e| e.into())
490
547
  }
491
548
 
549
+ fn as_mut_ptr(&self) -> *mut c_char {
550
+ // UNWRAP: This is actually infallible, but we need to
551
+ // wrap it in a `Result` for use with `inner()`.
552
+ self.inner(|inner| Ok(inner.as_mut_ptr() as *mut c_char))
553
+ .unwrap()
554
+ }
555
+
492
556
  /// Takes a closure with immutable access to InnerMmap. Will fail if the inner
493
557
  /// object has a mutable borrow or has been dropped.
494
558
  fn inner<F, T>(&self, func: F) -> Result<T>
@@ -544,9 +608,27 @@ impl MmapedFile {
544
608
  Ok(())
545
609
  }
546
610
 
611
+ /// Check if an RString is shared. Shared string use the same underlying
612
+ /// storage as their parent, taking an offset from the start. By default
613
+ /// they must run to the end of the parent string.
614
+ fn rb_string_is_shared(rb_str: RString) -> bool {
615
+ // SAFETY: We only hold a reference to the raw object for the duration
616
+ // of this function, and no Ruby code is called.
617
+ let flags = unsafe {
618
+ let raw_str = Self::rb_string_internal(rb_str);
619
+ raw_str.as_ref().basic.flags
620
+ };
621
+ let shared_flags = STR_SHARED | STR_NOEMBED;
622
+
623
+ flags & shared_flags == shared_flags
624
+ }
625
+
547
626
  /// Convert `magnus::RString` into the raw binding used by `rb_sys::RString`.
548
627
  /// We need this to manually change the pointer and length values for strings
549
628
  /// when moving the mmap to a new file.
629
+ ///
630
+ /// SAFETY: Calling Ruby code while the returned object is held may result
631
+ /// in it being mutated or dropped.
550
632
  unsafe fn rb_string_internal(rb_str: RString) -> NonNull<rb_sys::RString> {
551
633
  mem::transmute::<RString, NonNull<rb_sys::RString>>(rb_str)
552
634
  }
@@ -666,18 +748,101 @@ mod test {
666
748
  let ruby = magnus::Ruby::get().unwrap();
667
749
  crate::init(&ruby).unwrap();
668
750
 
751
+ fn assert_internals(
752
+ obj: Obj<MmapedFile>,
753
+ parent_id: c_ulong,
754
+ child_id: c_ulong,
755
+ unshared_id: c_ulong,
756
+ ) {
757
+ let rs_self = &*obj;
758
+ let tracker: Value = obj.ivar_get("@weak_obj_tracker").unwrap();
759
+
760
+ let mmap_ptr = rs_self.as_mut_ptr();
761
+ let mmap_len = rs_self.capacity();
762
+
763
+ let mut parent_checked = false;
764
+ let mut child_checked = false;
765
+
766
+ for val in tracker.enumeratorize("each_value", ()) {
767
+ let rb_string = val.unwrap();
768
+ let str = RString::from_value(rb_string).unwrap();
769
+
770
+ unsafe {
771
+ let raw_str = MmapedFile::rb_string_internal(str);
772
+ if str.as_raw() == child_id {
773
+ assert_eq!(parent_id, raw_str.as_ref().as_.heap.aux.shared);
774
+
775
+ let child_offset =
776
+ mmap_len as isize - raw_str.as_ref().as_.heap.len as isize;
777
+ assert_eq!(mmap_ptr.offset(child_offset), raw_str.as_ref().as_.heap.ptr);
778
+
779
+ child_checked = true;
780
+ } else if str.as_raw() == parent_id {
781
+ assert_eq!(parent_id, str.as_raw());
782
+
783
+ assert_eq!(mmap_ptr, raw_str.as_ref().as_.heap.ptr);
784
+ assert_eq!(mmap_len as c_long, raw_str.as_ref().as_.heap.len);
785
+ assert!(raw_str.as_ref().basic.flags & (STR_SHARED | STR_NOEMBED) > 0);
786
+ assert!(str.is_frozen());
787
+
788
+ parent_checked = true;
789
+ } else if str.as_raw() == unshared_id {
790
+ panic!("tracking unshared string");
791
+ } else {
792
+ panic!("unknown string");
793
+ }
794
+ }
795
+ }
796
+ assert!(parent_checked && child_checked);
797
+ }
798
+
669
799
  let obj = create_obj();
670
800
  let _ = populate_entries(&obj);
671
801
 
672
802
  let rs_self = &*obj;
673
803
 
674
- let value_range = Range::new(HEADER_SIZE, 24, false).unwrap().as_value();
675
- let value_slice = MmapedFile::slice(obj, &[value_range]).unwrap();
676
-
677
- rs_self.expand_to_fit(obj, rs_self.capacity() * 2).unwrap();
804
+ // Create a string containing the full mmap.
805
+ let parent_str = rs_self.str(obj).unwrap();
806
+ let parent_id = parent_str.as_raw();
807
+
808
+ // Ruby's shared strings are only created when they go to the end of
809
+ // original string.
810
+ let len = rs_self.inner(|inner| Ok(inner.len())).unwrap();
811
+ let shareable_range = Range::new(1, len - 1, false).unwrap().as_value();
812
+
813
+ // This string should re-use the parent's buffer with an offset and have
814
+ // the parent's id in `as.heap.aux.shared`
815
+ let child_str = rs_self._slice(obj, parent_str, &[shareable_range]).unwrap();
816
+ let child_id = child_str.as_raw();
817
+
818
+ // A range that does not reach the end of the parent will not be shared.
819
+ assert!(len > 4);
820
+ let unshareable_range = Range::new(0, 4, false).unwrap().as_value();
821
+
822
+ // This string should NOT be tracked, it should own its own buffer.
823
+ let unshared_str = rs_self
824
+ ._slice(obj, parent_str, &[unshareable_range])
825
+ .unwrap();
826
+ let unshared_id = unshared_str.as_raw();
827
+ assert!(!MmapedFile::rb_string_is_shared(unshared_str));
828
+
829
+ assert_internals(obj, parent_id, child_id, unshared_id);
830
+
831
+ let orig_ptr = rs_self.as_mut_ptr();
832
+ // Expand a bunch to ensure we remap
833
+ for _ in 0..16 {
834
+ rs_self.expand_to_fit(obj, rs_self.capacity() * 2).unwrap();
835
+ }
836
+ let new_ptr = rs_self.as_mut_ptr();
837
+ assert!(orig_ptr != new_ptr);
678
838
 
679
839
  // If we haven't updated the pointer to the newly remapped file this will segfault.
680
- let _: Value = eval!("puts str", str = value_slice).unwrap();
840
+ let _: Value = eval!("puts parent", parent = parent_str).unwrap();
841
+ let _: Value = eval!("puts child", child = child_str).unwrap();
842
+ let _: Value = eval!("puts unshared", unshared = unshared_str).unwrap();
843
+
844
+ // Confirm that tracked strings are still valid.
845
+ assert_internals(obj, parent_id, child_id, unshared_id);
681
846
  }
682
847
 
683
848
  #[test]
@@ -2,7 +2,7 @@ use smallvec::SmallVec;
2
2
  use std::str;
3
3
 
4
4
  /// String slices pointing to the fields of a borrowed `Entry`'s JSON data.
5
- #[derive(PartialEq, Debug)]
5
+ #[derive(PartialEq, Eq, Debug)]
6
6
  pub struct MetricText<'a> {
7
7
  pub family_name: &'a str,
8
8
  pub metric_name: &'a str,
@@ -10,14 +10,14 @@ pub struct MetricText<'a> {
10
10
  pub values: SmallVec<[&'a str; 4]>,
11
11
  }
12
12
 
13
- #[derive(PartialEq, Debug)]
13
+ #[derive(PartialEq, Eq, Debug)]
14
14
  struct MetricNames<'a> {
15
15
  label_json: &'a str,
16
16
  family_name: &'a str,
17
17
  metric_name: &'a str,
18
18
  }
19
19
 
20
- #[derive(PartialEq, Debug)]
20
+ #[derive(PartialEq, Eq, Debug)]
21
21
  struct MetricLabelVals<'a> {
22
22
  labels: SmallVec<[&'a str; 4]>,
23
23
  values: SmallVec<[&'a str; 4]>,
@@ -63,35 +63,35 @@ fn parse_names(json: &str) -> Option<MetricNames> {
63
63
  return None;
64
64
  }
65
65
 
66
- // Now: family_name","metric_name",[...
67
- let remainder = json.get(2..)?;
68
-
69
- let names_end = remainder.find('[')?;
70
-
71
- // Save the rest of the slice to parse for labels later.
72
- let label_json = remainder.get(names_end..)?;
73
-
74
- // Now: family_name","metric_name",
75
- let remainder = remainder.get(..names_end)?;
66
+ // Captured: "family_name","metric_name",
67
+ // ^^^^^^^^^^^
68
+ let (family_name, remainder) = scan_str(json.get(1..)?)?;
76
69
 
77
- // Split on commas into:
78
- // family_name","metric_name",
79
- // ^^^^one^^^^^ ^^^^^two^^^^^
80
- let mut token_iter = remainder.split(',');
70
+ // Validate comma separated names
71
+ // ["family_name","metric_name",[...
72
+ // ^
73
+ if !remainder.starts_with("\",") {
74
+ return None;
75
+ }
81
76
 
82
- // Captured: family_name","metric_name",
83
- // ^^^^^^^^^^^
84
- let family_name = token_iter.next()?.trim_end_matches('"');
77
+ // Now: "metric_name",[...
78
+ let remainder = remainder.get(2..)?;
85
79
 
86
80
  // Captured: "family_name","metric_name",
87
81
  // ^^^^^^^^^^^
88
- let metric_name = token_iter.next()?.trim_matches('"');
82
+ let (metric_name, remainder) = scan_str(remainder)?;
89
83
 
90
- // Confirm the final entry of the iter is empty, the the trailing ','.
91
- if !token_iter.next()?.is_empty() {
84
+ // Validate comma separated names
85
+ // ["family_name","metric_name",[...
86
+ // ^^
87
+ if !remainder.starts_with("\",") {
92
88
  return None;
93
89
  }
94
90
 
91
+ // Save the rest of the slice to parse for labels later.
92
+ // Now: [...
93
+ let label_json = remainder.get(2..)?;
94
+
95
95
  Some(MetricNames {
96
96
  label_json,
97
97
  family_name,
@@ -101,76 +101,143 @@ fn parse_names(json: &str) -> Option<MetricNames> {
101
101
 
102
102
  fn parse_label_values(json: &str) -> Option<MetricLabelVals> {
103
103
  // Starting with: ["label_a","label_b"],["value_a", "value_b"]]
104
- if !(json.starts_with('[') && json.ends_with("]]")) {
104
+ if !json.starts_with('[') {
105
105
  return None;
106
106
  }
107
107
 
108
+ // Now: "label_a","label_b"],["value_a", "value_b"]]
109
+ let mut remainder = json.get(1..)?;
110
+
108
111
  // Validate we either have the start of a label string or an
109
112
  // empty array, e.g. `["` or `[]`.
110
113
  if !matches!(json.as_bytes().get(1)?, b'"' | b']') {
111
114
  return None;
112
115
  }
113
116
 
114
- // Now: "label_a","label_b"
115
- let labels_end = json.find(']')?;
116
- let label_range = json.get(1..labels_end)?;
117
-
118
117
  let mut labels = SmallVec::new();
119
118
 
120
119
  // Split on commas into:
121
120
  // "label_a","label_b"
122
121
  // ^^^one^^^ ^^^two^^^
123
- for label in label_range.split(',') {
124
- // Captured: "label_a","label_b"
125
- // ^^^^^^^
126
- // If there are no labels, e.g. `[][]`, then don't capture anything.
127
- if !label.is_empty() {
128
- labels.push(label.trim_matches('"'));
129
- }
122
+ loop {
123
+ let Some((label, label_rem)) = scan_str(remainder) else {
124
+ // No further keys.
125
+ break;
126
+ };
127
+ labels.push(label);
128
+
129
+ // Advance past trailing quote.
130
+ remainder = label_rem.get(1..)?;
131
+ match remainder.as_bytes().first() {
132
+ Some(b']') => break, // No further labels.
133
+ Some(b',') => {} // More labels expected.
134
+ _ => return None, // Invalid.
135
+ };
136
+
137
+ // Advance past comma.
138
+ remainder = remainder.get(1..)?;
130
139
  }
131
140
 
132
- // Now: ],["value_a", "value_b"]]
133
- let mut values_range = json.get(labels_end..)?;
134
-
135
- // Validate we have a separating comma with one and only one leading bracket.
136
- if !(values_range.starts_with("],[") && values_range.as_bytes().get(3)? != &b'[') {
141
+ if !remainder.starts_with("],[") {
137
142
  return None;
138
143
  }
139
-
140
144
  // Now: "value_a", "value_b"]]
141
- values_range = values_range.get(3..)?;
142
-
143
- let values_end = values_range.find(']')?;
145
+ remainder = remainder.get(3..)?;
144
146
 
145
- // Validate we have only two trailing brackets.
146
- if values_range.get(values_end..)?.len() > 2 {
147
+ // Validate we don't have extra brackets.
148
+ if remainder.starts_with('[') {
147
149
  return None;
148
150
  }
149
151
 
150
- // Now: "value_a", "value_b"
151
- values_range = values_range.get(..values_end)?;
152
-
153
152
  let mut values = SmallVec::new();
154
-
155
153
  // Split on commas into:
156
154
  // "value_a","value_b"
157
155
  // ^^^one^^^ ^^^two^^^
158
- for value in values_range.split(',') {
159
- // Captured: "value_a","value_b"
160
- // ^^^^^^^^^
161
- // If there are no values, e.g. `[][]`, then don't capture anything.
162
- if !value.is_empty() {
163
- values.push(value.trim_matches('"'));
156
+ loop {
157
+ if remainder.starts_with('"') {
158
+ let (value, value_rem) = scan_str(remainder)?;
159
+ values.push(value);
160
+
161
+ // Advance past trailing quote.
162
+ remainder = value_rem.get(1..)?;
163
+ } else {
164
+ // An unquoted value, such as `true` or `404`.
165
+ let i = remainder.find(|c| c == ',' || c == ']')?;
166
+ let value = &remainder[..i];
167
+
168
+ match (value.is_empty(), is_valid_json_literal(value)) {
169
+ (true, _) => {} // Empty string, do nothing.
170
+ (false, true) => values.push(value), // Valid literal.
171
+ (false, false) => return None, // Invalid literal, fail.
172
+ }
173
+
174
+ remainder = &remainder[i..];
164
175
  }
176
+
177
+ match remainder.as_bytes().first() {
178
+ Some(b']') => break, // End of values.
179
+ Some(b',') => {} // More values expected.
180
+ _ => return None, // Invalid.
181
+ };
182
+
183
+ // Advance past comma.
184
+ remainder = remainder.get(1..)?;
165
185
  }
166
186
 
167
187
  if values.len() != labels.len() {
168
188
  return None;
169
189
  }
170
190
 
191
+ // Now: ]]
192
+ if remainder != "]]" {
193
+ return None;
194
+ }
195
+
171
196
  Some(MetricLabelVals { labels, values })
172
197
  }
173
198
 
199
+ // Read a JSON-encoded str that includes starting and ending double quotes.
200
+ // Returns the validated str with the double quotes trimmed and the remainder
201
+ // of the input str.
202
+ fn scan_str(json: &str) -> Option<(&str, &str)> {
203
+ let mut escaping = false;
204
+
205
+ if !json.starts_with('"') {
206
+ return None;
207
+ }
208
+
209
+ // Trim leading double quote.
210
+ let json = json.get(1..)?;
211
+
212
+ for (i, &c) in json.as_bytes().iter().enumerate() {
213
+ if c == b'\\' {
214
+ escaping = true;
215
+ continue;
216
+ }
217
+
218
+ if c == b'"' && !escaping {
219
+ return Some((json.get(..i)?, json.get(i..)?));
220
+ }
221
+
222
+ escaping = false;
223
+ }
224
+
225
+ None
226
+ }
227
+
228
+ // Confirm an unquoted JSON literal is a boolean, null, or has a passing
229
+ // resemblance to a number. We do not confirm numeric formatting, only
230
+ // that all characters are valid. See https://www.json.org/json-en.html
231
+ // for details.
232
+ fn is_valid_json_literal(s: &str) -> bool {
233
+ match s {
234
+ "true" | "false" | "null" => true,
235
+ _ => s.chars().all(|c| {
236
+ c.is_ascii_digit() || c == '.' || c == '+' || c == '-' || c == 'e' || c == 'E'
237
+ }),
238
+ }
239
+ }
240
+
174
241
  #[cfg(test)]
175
242
  mod test {
176
243
  use smallvec::smallvec;
@@ -217,6 +284,16 @@ mod test {
217
284
  values: smallvec!["value_a", "403"],
218
285
  }),
219
286
  },
287
+ TestCase {
288
+ name: "scientific notation literal",
289
+ input: r#"["metric","name",["label_a"],[-2.0e-5]]"#,
290
+ expected: Some(MetricText {
291
+ family_name: "metric",
292
+ metric_name: "name",
293
+ labels: smallvec!["label_a"],
294
+ values: smallvec!["-2.0e-5"],
295
+ }),
296
+ },
220
297
  TestCase {
221
298
  name: "null value",
222
299
  input: r#"["metric","name",["label_a","label_b"],[null,"value_b"]]"#,
@@ -237,6 +314,36 @@ mod test {
237
314
  values: smallvec![],
238
315
  }),
239
316
  },
317
+ TestCase {
318
+ name: "comma in items",
319
+ input: r#"["met,ric","na,me",["label,_a","label_b"],["value,_a","value_b"]]"#,
320
+ expected: Some(MetricText {
321
+ family_name: "met,ric",
322
+ metric_name: "na,me",
323
+ labels: smallvec!["label,_a", "label_b"],
324
+ values: smallvec!["value,_a", "value_b"],
325
+ }),
326
+ },
327
+ TestCase {
328
+ name: "bracket in value",
329
+ input: r#"["met[r]ic","na[m]e",["label[_]a","label_b"],["value_a","val[ue]_b"]]"#,
330
+ expected: Some(MetricText {
331
+ family_name: "met[r]ic",
332
+ metric_name: "na[m]e",
333
+ labels: smallvec!["label[_]a", "label_b"],
334
+ values: smallvec!["value_a", "val[ue]_b"],
335
+ }),
336
+ },
337
+ TestCase {
338
+ name: "escaped quotes",
339
+ input: r#"["met\"ric","na\"me",["label\"_a","label_b"],["value\"_a","value_b"]]"#,
340
+ expected: Some(MetricText {
341
+ family_name: r#"met\"ric"#,
342
+ metric_name: r#"na\"me"#,
343
+ labels: smallvec![r#"label\"_a"#, "label_b"],
344
+ values: smallvec![r#"value\"_a"#, "value_b"],
345
+ }),
346
+ },
240
347
  ];
241
348
 
242
349
  for case in tc {
@@ -313,23 +420,18 @@ mod test {
313
420
  expected: None,
314
421
  },
315
422
  TestCase {
316
- name: "comma in family name",
317
- input: r#"["met,ric","name",["label_a","label_b"],["value_a","value_b"]]"#,
318
- expected: None,
319
- },
320
- TestCase {
321
- name: "comma in metric name",
322
- input: r#"["metric","na,me",["label_a","label_b"],["value_a","value_b"]]"#,
423
+ name: "misplaced bracket",
424
+ input: r#"["metric","name",["label_a","label_b"],]["value_a","value_b"]]"#,
323
425
  expected: None,
324
426
  },
325
427
  TestCase {
326
- name: "comma in value",
327
- input: r#"["metric","na,me",["label_a","label_b"],["val,ue_a","value_b"]]"#,
428
+ name: "comma in numeric value",
429
+ input: r#"["metric","name",["label_a","label_b"],[400,0,"value_b"]]"#,
328
430
  expected: None,
329
431
  },
330
432
  TestCase {
331
- name: "comma in numeric value",
332
- input: r#"["metric","name",["label_a","label_b"],[400,0,"value_b"]]"#,
433
+ name: "non-e letter in numeric value",
434
+ input: r#"["metric","name",["label_a","label_b"],[400x0,"value_b"]]"#,
333
435
  expected: None,
334
436
  },
335
437
  ];
@@ -6,7 +6,7 @@ use crate::util::CheckedOps;
6
6
  use crate::Result;
7
7
 
8
8
  /// The logic to save a `MetricsEntry`, or parse one from a byte slice.
9
- #[derive(PartialEq, Clone, Debug)]
9
+ #[derive(PartialEq, Eq, Clone, Debug)]
10
10
  pub struct RawEntry<'a> {
11
11
  bytes: &'a [u8],
12
12
  encoded_len: usize,
@@ -73,7 +73,7 @@ pub fn entries_to_db(entries: &[&'static str], values: &[f64], header: Option<u3
73
73
  };
74
74
 
75
75
  out.extend(used.to_ne_bytes());
76
- out.extend(&[0x0u8; 4]); // Padding.
76
+ out.extend([0x0u8; 4]); // Padding.
77
77
  out.extend(entry_bytes);
78
78
 
79
79
  out
@@ -7,13 +7,14 @@ require 'tmpdir'
7
7
  module Prometheus
8
8
  module Client
9
9
  class Configuration
10
- attr_accessor :value_class, :multiprocess_files_dir, :initial_mmap_file_size, :logger, :pid_provider
10
+ attr_accessor :value_class, :multiprocess_files_dir, :initial_mmap_file_size, :logger, :pid_provider, :rust_multiprocess_metrics
11
11
 
12
12
  def initialize
13
13
  @value_class = ::Prometheus::Client::MmapedValue
14
14
  @initial_mmap_file_size = ::Prometheus::Client::PageSize.page_size(fallback_page_size: 4096)
15
15
  @logger = Logger.new($stdout)
16
16
  @pid_provider = Process.method(:pid)
17
+ @rust_multiprocess_metrics = ENV.fetch('prometheus_rust_multiprocess_metrics', nil) == 'true'
17
18
  @multiprocess_files_dir = ENV.fetch('prometheus_multiproc_dir') do
18
19
  Dir.mktmpdir("prometheus-mmap")
19
20
  end
@@ -1,5 +1,6 @@
1
1
  require 'prometheus/client/uses_value_type'
2
2
  require 'prometheus/client/helper/json_parser'
3
+ require 'prometheus/client/helper/loader'
3
4
  require 'prometheus/client/helper/plain_file'
4
5
  require 'prometheus/client/helper/metrics_processing'
5
6
  require 'prometheus/client/helper/metrics_representation'
@@ -31,7 +32,7 @@ module Prometheus
31
32
  .map {|f| Helper::PlainFile.new(f) }
32
33
  .map {|f| [f.filepath, f.multiprocess_mode.to_sym, f.type.to_sym, f.pid] }
33
34
 
34
- if use_rust && rust_impl_available?
35
+ if use_rust && Prometheus::Client::Helper::Loader.rust_impl_available?
35
36
  FastMmapedFileRs.to_metrics(file_list.to_a)
36
37
  else
37
38
  FastMmapedFile.to_metrics(file_list.to_a)
@@ -46,29 +47,6 @@ module Prometheus
46
47
 
47
48
  private
48
49
 
49
- def load_rust_extension
50
- begin
51
- ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
52
- require_relative "../../../#{ruby_version}/fast_mmaped_file_rs"
53
- rescue LoadError
54
- require 'fast_mmaped_file_rs'
55
- end
56
- end
57
-
58
- def check_for_rust
59
- # This will be evaluated on each invocation even with `||=` if
60
- # `@rust_available` if false. Running a `require` statement is slow,
61
- # so the `rust_impl_available?` method memoizes the result, external
62
- # callers can only trigger this method a single time.
63
- @rust_available = begin
64
- load_rust_extension
65
- true
66
- rescue LoadError
67
- Prometheus::Client.logger.info('FastMmapedFileRs unavailable')
68
- false
69
- end
70
- end
71
-
72
50
  def load_metrics(path)
73
51
  metrics = {}
74
52
  Dir.glob(File.join(path, '*.db')).sort.each do |f|
@@ -0,0 +1,40 @@
1
+ module Prometheus
2
+ module Client
3
+ module Helper
4
+ module Loader
5
+ class << self
6
+ def rust_impl_available?
7
+ return @rust_available unless @rust_available.nil?
8
+
9
+ check_for_rust
10
+ end
11
+
12
+ private
13
+
14
+ def load_rust_extension
15
+ begin
16
+ ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
17
+ require_relative "../../../#{ruby_version}/fast_mmaped_file_rs"
18
+ rescue LoadError
19
+ require 'fast_mmaped_file_rs'
20
+ end
21
+ end
22
+
23
+ def check_for_rust
24
+ # This will be evaluated on each invocation even with `||=` if
25
+ # `@rust_available` if false. Running a `require` statement is slow,
26
+ # so the `rust_impl_available?` method memoizes the result, external
27
+ # callers can only trigger this method a single time.
28
+ @rust_available = begin
29
+ load_rust_extension
30
+ true
31
+ rescue LoadError
32
+ Prometheus::Client.logger.info('FastMmapedFileRs unavailable')
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,5 +1,6 @@
1
1
  require 'prometheus/client/helper/entry_parser'
2
2
  require 'prometheus/client/helper/file_locker'
3
+ require 'prometheus/client/helper/loader'
3
4
 
4
5
  # load precompiled extension if available
5
6
  begin
@@ -12,7 +13,17 @@ end
12
13
  module Prometheus
13
14
  module Client
14
15
  module Helper
15
- class MmapedFile < FastMmapedFile
16
+ # We can't check `Prometheus::Client.configuration` as this creates a circular dependency
17
+ if (ENV.fetch('prometheus_rust_mmaped_file', nil) == "true" &&
18
+ Prometheus::Client::Helper::Loader.rust_impl_available?)
19
+ class MmapedFile < FastMmapedFileRs
20
+ end
21
+ else
22
+ class MmapedFile < FastMmapedFile
23
+ end
24
+ end
25
+
26
+ class MmapedFile
16
27
  include EntryParser
17
28
 
18
29
  attr_reader :filepath, :size
@@ -62,8 +62,10 @@ module Prometheus
62
62
  end
63
63
 
64
64
  def respond_with(format)
65
+ rust_enabled = Prometheus::Client.configuration.rust_multiprocess_metrics
66
+
65
67
  response = if Prometheus::Client.configuration.value_class.multiprocess
66
- format.marshal_multiprocess
68
+ format.marshal_multiprocess(use_rust: rust_enabled)
67
69
  else
68
70
  format.marshal
69
71
  end
@@ -1,5 +1,5 @@
1
1
  module Prometheus
2
2
  module Client
3
- VERSION = '0.23.1'.freeze
3
+ VERSION = '0.24.4'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prometheus-client-mmap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.1
4
+ version: 0.24.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Schmidt
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2023-05-02 00:00:00.000000000 Z
14
+ date: 2023-06-12 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rb_sys
@@ -174,6 +174,7 @@ files:
174
174
  - lib/prometheus/client/helper/entry_parser.rb
175
175
  - lib/prometheus/client/helper/file_locker.rb
176
176
  - lib/prometheus/client/helper/json_parser.rb
177
+ - lib/prometheus/client/helper/loader.rb
177
178
  - lib/prometheus/client/helper/metrics_processing.rb
178
179
  - lib/prometheus/client/helper/metrics_representation.rb
179
180
  - lib/prometheus/client/helper/mmaped_file.rb