Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions prometheus_client/mmap_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ def read_all_values_from_file(filename):
# Read the first block of data, including the first 4 bytes which tell us
# how much of the file (which is preallocated to _INITIAL_MMAP_SIZE bytes) is occupied.
data = infp.read(mmap.PAGESIZE)
if not data:
# __init__ creates the file and only truncates it to
# _INITIAL_MMAP_SIZE afterwards, so a reader can observe it while
# it is still empty; a process dying in that window can also leave
# an empty file behind for good. Treat it as a file with nothing
# recorded in it yet, mirroring the `capacity == 0` case in
# __init__, rather than failing to unpack a header that isn't
# there. Anything longer than this is left to fail loudly.
return iter(())
used = _unpack_integer(data, 0)[0]
if used > len(data): # Then read in the rest, if needed.
data += infp.read(used - len(data))
Expand Down
47 changes: 47 additions & 0 deletions tests/test_multiprocess.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import glob
import os
import shutil
import struct
import tempfile
import unittest
import warnings
Expand Down Expand Up @@ -477,6 +478,26 @@ def test_missing_gauge_file_during_merge(self):
os.path.join(self.tempdir, 'gauge_livesum_9999999.db'),
]))

def test_uninitialized_file_during_merge(self):
# This file exists but is still zero bytes, just like a file created by
# MmapedDict.__init__ that has not been truncated to its initial size
# yet, or one left behind by a process that died in that window.
# This should not raise and return no metrics
empty_file = os.path.join(self.tempdir, 'counter_9999999.db')
open(empty_file, 'wb').close()

self.assertFalse(self.collector.merge([empty_file]))

def test_uninitialized_file_does_not_hide_other_metrics(self):
# A single unreadable file must not take the whole collection with it,
# as an empty file can be left behind indefinitely: it is named after a
# pid that never comes back, so nothing cleans it up.
c = Counter('c', 'help', registry=None)
c.inc(1)
open(os.path.join(self.tempdir, 'counter_9999999.db'), 'wb').close()

self.assertEqual(1, self.registry.get_sample_value('c_total'))

def test_remove_clear_warning(self):
os.environ['PROMETHEUS_MULTIPROC_DIR'] = self.tempdir
with warnings.catch_warnings(record=True) as w:
Expand Down Expand Up @@ -638,6 +659,32 @@ def test_corruption_detected(self):
with self.assertRaises(RuntimeError):
list(self.d.read_all_values())

def test_read_all_values_from_empty_file(self):
# A reader can observe the file in the window between __init__ creating
# it and truncating it to _INITIAL_MMAP_SIZE, and a process dying in
# that window leaves it empty for good. Either way it holds no values,
# so it should read as empty rather than failing to unpack a header
# that was never written.
fd, empty_file = tempfile.mkstemp()
os.close(fd)
try:
self.assertEqual([], list(mmap_dict.MmapedDict.read_all_values_from_file(empty_file)))
finally:
os.unlink(empty_file)

def test_read_all_values_from_file_with_truncated_contents(self):
# Only a wholly empty file is tolerated: a file claiming to hold more
# than it does is corrupt, and should still be reported as such.
fd, short_file = tempfile.mkstemp()
os.close(fd)
try:
with open(short_file, 'wb') as outfp:
outfp.write(mmap_dict._pack_integer_func(100))
with self.assertRaises(struct.error):
list(mmap_dict.MmapedDict.read_all_values_from_file(short_file))
finally:
os.unlink(short_file)

def tearDown(self):
self.d.close()
os.unlink(self.tempfile)
Expand Down