require 'prometheus/client/helper/mmaped_file' require 'prometheus/client' module Prometheus module Client class ParsingError < StandardError end # A dict of doubles, backed by an mmapped file. # # The file starts with a 4 byte int, indicating how much of it is used. # Then 4 bytes of padding. # There's then a number of entries, consisting of a 4 byte int which is the # size of the next field, a utf-8 encoded string key, padding to an 8 byte # alignment, and then a 8 byte float which is the value. # # For example, for : # # 0: | PID (4 bytes) | Used bytes (4 bytes) | # 1: | Padding (4 bytes) | Size of field (4 bytes) | # 2: # 2: | Entry 1 key (N bytes) | Padding (4 bytes) | # 3: | Entry 1 value (8 bytes) | # 4: | Size of field 2 | Padding (4 bytes) | # 5: | Entry 2 key (4 bytes) | Padding (4 byte) | # 6: | Entry 2 value (8 bytes) | class MmapedDict MINIMUM_SIZE = 8 attr_reader :m, :used, :positions def initialize(m) @mutex = Mutex.new @m = m # @m.mlock # TODO: Ensure memory is locked to RAM @used = @m.used @positions = {} read_all_positions.each do |key, pos| @positions[key] = pos end rescue StandardError => e raise ParsingError, "exception #{e} while processing metrics file #{path}" end # Yield (key, value). No locking is performed. def self.read_all_values(f) m = Helper::MmapedFile.open(f) m.entries.map do |data, encoded_len, value_offset, _| encoded, value = data.unpack(format('@4A%d@%dd', encoded_len, value_offset)) [encoded, value] end ensure m.munmap end def read_value(key) @mutex.synchronize do init_value(key) unless @positions.key?(key) end pos = @positions[key] # We assume that reading from an 8 byte aligned value is atomic. @m[pos..pos + 7].unpack('d')[0] end def write_value(key, value) @mutex.synchronize do init_value(key) unless @positions.key?(key) end pos = @positions[key] # We assume that writing to an 8 byte aligned value is atomic. @m[pos..pos + 7] = [value].pack('d') end def path @m.filepath unless @m.nil? end def close @m.close rescue TypeError => e Prometheus::Client.logger.warn("munmap raised error #{e}") end private # Initialize a value. Lock must be held by caller. def init_value(key) @m.add_entry(key, 0.0) # Update how much space we've used. @used = @m.used @positions[key] = @used - 8 end # Yield (key, pos). No locking is performed. def read_all_positions @m.entries.map do |data, encoded_len, _, absolute_pos| encoded, = data.unpack(format('@4A%d', encoded_len)) [encoded, absolute_pos] end end end end end