implemented robust estimation of time-until-disc-full
authorChristian Herdtweck <christian.herdtweck@intra2net.com>
Wed, 13 Jan 2016 11:14:52 +0000 (12:14 +0100)
committerChristian Herdtweck <christian.herdtweck@intra2net.com>
Wed, 13 Jan 2016 11:14:52 +0000 (12:14 +0100)
test_helpers.py

index 42eff5c..8c95e0d 100644 (file)
@@ -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