58 lines
2.5 KiB
Python
58 lines
2.5 KiB
Python
"""Daily filesystem capacity cache, persisted in RAM across monitor restarts."""
|
|||
|
|
import json
|
||
|
|
import time
|
||
|
|
from pathlib import Path
|
||
|
|
from shutil import disk_usage
|
||
|
|
|
||
|
|
|
||
|
|
class StorageCache:
|
||
|
|
def __init__(self, path, reader=disk_usage, clock=time.time):
|
||
|
|
self.path, self.reader, self.clock = Path(path), reader, clock
|
||
|
|
self.values = None
|
||
|
|
self.observed_at = None
|
||
|
|
self.next_update_at = 0
|
||
|
|
self.error = False
|
||
|
|
try:
|
||
|
|
data = json.loads(self.path.read_text())
|
||
|
|
if not isinstance(data,dict):raise ValueError('invalid cache')
|
||
|
|
retry = data.get('next_update_at')
|
||
|
|
if data.get('error') is True and isinstance(retry,(int,float)) and self.clock() < retry <= self.clock()+3600:
|
||
|
|
self.error, self.next_update_at = True, retry
|
||
|
|
values = data['values']
|
||
|
|
if all(isinstance(values[k], int) and not isinstance(values[k], bool) and values[k] >= 0 for k in ('total', 'used', 'free')) and values['total'] > 0 and values['used'] <= values['total']:
|
||
|
|
at = data['observed_at']
|
||
|
|
if isinstance(at, (int, float)) and 0 <= self.clock() - at < 90000:
|
||
|
|
self.values, self.observed_at = values, at
|
||
|
|
if not self.error:self.next_update_at = at + 86400
|
||
|
|
except (OSError, ValueError, KeyError, TypeError):
|
||
|
|
pass
|
||
|
|
|
||
|
|
def sample(self):
|
||
|
|
now = self.clock()
|
||
|
|
if now < self.next_update_at:
|
||
|
|
return self.values
|
||
|
|
try:
|
||
|
|
usage = self.reader('/')
|
||
|
|
if usage.total <= 0 or not 0 <= usage.used <= usage.total or usage.free < 0:
|
||
|
|
raise ValueError('invalid capacity')
|
||
|
|
except (OSError, ValueError):
|
||
|
|
self.error = True
|
||
|
|
self.next_update_at = now + 3600
|
||
|
|
self.persist()
|
||
|
|
return self.values
|
||
|
|
self.values = dict(total=usage.total, used=usage.used, free=usage.free)
|
||
|
|
self.observed_at = now
|
||
|
|
self.next_update_at = now + 86400
|
||
|
|
self.error = False
|
||
|
|
self.persist()
|
||
|
|
return self.values
|
||
|
|
|
||
|
|
def persist(self):
|
||
|
|
try:
|
||
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
self.path.write_text(json.dumps(dict(values=self.values, observed_at=self.observed_at,
|
||
|
|
next_update_at=self.next_update_at, error=self.error)))
|
||
|
|
except OSError:
|
||
|
|
# The in-memory sample remains valid even if the RAM cache cannot be persisted.
|
||
|
|
pass
|