From 7bd4c7bfc941ede4c1673998ac2748566be87472 Mon Sep 17 00:00:00 2001 From: Christian Herdtweck Date: Wed, 13 Jan 2016 12:14:52 +0100 Subject: [PATCH] implemented robust estimation of time-until-disc-full --- test_helpers.py | 89 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 files changed, 77 insertions(+), 12 deletions(-) diff --git a/test_helpers.py b/test_helpers.py index 42eff5c..8c95e0d 100644 --- a/test_helpers.py +++ b/test_helpers.py @@ -12,25 +12,21 @@ from time import sleep from file_helpers import get_disc_stats from datetime import datetime as dt from buffers import LogarithmicBuffer +from itertools import tee class DiscFillCheckerThread(Thread): """ a thread that checks disc fill in regular intervals """ - def __init__(self, interval=10, warn_func=None, err_func=None): + def __init__(self, interval=10, decision_function = None): super(DiscFillCheckerThread, self).__init__(name='discFillChck', daemon=True) # set variables self.interval = interval - if warn_func is None: - self.warn_func = disc_fill_warn_func + if decision_function is None: + self.decision_function = default_disc_full_decision_function else: # need to check warn_func somehow (using introspection?) - self.warn_func = warn_func - if err_func is None: - self.err_func = disc_fill_err_func - else: - # need to check err_func somehow (using introspection?) - self.err_func = err_func + self.decision_function = decision_function # remember relevant disc stats at start bufs = dict((stat.name, LogarithmicBuffer(5)) for stat in stats) @@ -51,10 +47,79 @@ class DiscFillCheckerThread(Thread): # new file system -- need to create a new buffer buf = LogarithmicBuffer(5) - buf.add((now, stat.available)) + buf.add((now, float(stat.available))) + fill_states = buf.get_all() + + # estimate time until full (i.e. when available == 0) + times_until_empty = \ + [calc_times_until_zero(old_state, new_state) + for old_state, new_state in pairwise(fill_states)] + + # call user-defined function to decide + min_time = None + if any(times_until_empty > 0): + min_time = min(time for time in times_until_empty + if time>0) + avg_time = calc_times_until_zero(fill_states[0], + fill_states[-1]) + self.decision_function(stat, + times_until_empty[-1], # newest + min(times_until_empty), # smallest + avg_time) # average + + +def default_disc_full_decision_function(curr_state, + estim_empty_newest, + estim_empty_smallest, + estim_empty_mean): + """ decide whether to warn or raise error because disc is getting full + + called by DiscFillCheckerThread regularly; params estim_empty_* are result + of :py:func:`calc_times_until_zero`, so None means that disc state did not + change. + + :param estim_empty_newest: the newest time estimate + :param estim_empty_smallest: the smallest of all non-negative estimates + :param estim_empty_mean: estim between earliest and newest estimate + """ + pass + +def calc_times_until_zero(old_state, new_state): + """ calc time until value hits zero given (time1, value1), (time2, value2) + + returns time from now until empty in seconds + + delta_date = new_date - old_date + delta_free = new_free - old_free + free(t) = new_free + (t-new_t) * delta_free / delta_date + = 0 <==> + diff_t := new_t - t = new_free * delta_date / delta_free + + compare to now: + diff_to_now = t + (-new_t + new_t) - now = (t - new_t) + (new_t - now) + = -diff_t - (now-new_t) + + To avoid zero-division, returns None if delta_free == 0 + """ + old_date, old_free = old_state + new_date, new_free = new_state + if new_free == old_free: + return None # avoid zero-division + time_diff = new_free * (new_date - old_date).total_seconds() \ + / (old_free - new_free) + + # compare to now + return (now - new_date).total_seconds() - time_diff + + +def pairwise(iterable): + """ s -> (s0,s1), (s1,s2), (s2, s3), ... - raise NotImplementedError('estimate time until disc full') - raise NotImplementedError('warn or err if too soon') + taken from itertool recipes in python api doc of itertools + """ + a, b = tee(iterable) + next(b, None) + return zip(a, b) @contextlib.contextmanager -- 1.7.1