From ac46013fcc5520b628631797b9d5f4db50174220 Mon Sep 17 00:00:00 2001 From: chris meyers Date: Fri, 24 Apr 2020 14:22:46 -0400 Subject: [PATCH] fix sliding window per/minute calc * Add tests to ensure correctness --- awx/main/analytics/broadcast_websocket.py | 11 ++- .../analytics/test_broadcast_websocket.py | 69 +++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 awx/main/tests/unit/analytics/test_broadcast_websocket.py diff --git a/awx/main/analytics/broadcast_websocket.py b/awx/main/analytics/broadcast_websocket.py index 449726f3fe..5cfda529eb 100644 --- a/awx/main/analytics/broadcast_websocket.py +++ b/awx/main/analytics/broadcast_websocket.py @@ -44,8 +44,8 @@ class FixedSlidingWindow(): def cleanup(self, now_bucket=None): now_bucket = now_bucket or now_seconds() - if self.start_time + 60 <= now_bucket: - self.start_time = now_bucket + 60 + 1 + if self.start_time + 60 < now_bucket: + self.start_time = now_bucket - 60 # Delete old entries for k in list(self.buckets.keys()): @@ -53,16 +53,15 @@ class FixedSlidingWindow(): del self.buckets[k] def record(self, ts=None): - ts = ts or datetime.datetime.now() - now_bucket = int((ts - datetime.datetime(1970,1,1)).total_seconds()) + now_bucket = ts or dt_to_seconds(datetime.datetime.now()) val = self.buckets.get(now_bucket, 0) self.buckets[now_bucket] = val + 1 self.cleanup(now_bucket) - def render(self): - self.cleanup() + def render(self, ts=None): + self.cleanup(now_bucket=ts) return sum(self.buckets.values()) or 0 diff --git a/awx/main/tests/unit/analytics/test_broadcast_websocket.py b/awx/main/tests/unit/analytics/test_broadcast_websocket.py new file mode 100644 index 0000000000..6edfe51b92 --- /dev/null +++ b/awx/main/tests/unit/analytics/test_broadcast_websocket.py @@ -0,0 +1,69 @@ +import datetime + +from awx.main.analytics.broadcast_websocket import FixedSlidingWindow +from awx.main.analytics.broadcast_websocket import dt_to_seconds + + +class TestFixedSlidingWindow(): + + def ts(self, **kwargs): + e = { + 'year': 1985, + 'month': 1, + 'day': 1, + 'hour': 1, + } + return dt_to_seconds(datetime.datetime(**kwargs, **e)) + + def test_record_same_minute(self): + """ + Legend: + - = record() + ^ = render() + |---| = 1 minute, 60 seconds + + .................... + |------------------------------------------------------------| + ^^^^^^^^^^^^^^^^^^^^ + """ + + fsw = FixedSlidingWindow(self.ts(minute=0, second=0, microsecond=0)) + for i in range(20): + fsw.record(self.ts(minute=0, second=i, microsecond=0)) + assert (i + 1) == fsw.render(self.ts(minute=0, second=i, microsecond=0)) + + + def test_record_same_minute_render_diff_minute(self): + """ + Legend: + - = record() + ^ = render() + |---| = 1 minute, 60 seconds + + .................... + |------------------------------------------------------------| + ^^ ^ + AB C + |------------------------------------------------------------| + ^^^^^^^^^^^^^^^^^^^^^ + DEEEEEEEEEEEEEEEEEEEF + """ + + fsw = FixedSlidingWindow(self.ts(minute=0, second=0, microsecond=0)) + for i in range(20): + fsw.record(self.ts(minute=0, second=i, microsecond=0)) + + assert 20 == fsw.render(self.ts(minute=0, second=19, microsecond=0)), \ + "A. The second of the last record() call" + assert 20 == fsw.render(self.ts(minute=0, second=20, microsecond=0)), \ + "B. The second after the last record() call" + assert 20 == fsw.render(self.ts(minute=0, second=59, microsecond=0)), \ + "C. Last second in the same minute that all record() called in" + assert 20 == fsw.render(self.ts(minute=1, second=0, microsecond=0)), \ + "D. First second of the minute following the minute that all record() calls in" + for i in range(20): + assert 20 - i == fsw.render(self.ts(minute=1, second=i, microsecond=0)), \ + "E. Sliding window where 1 record() should drop from the results each time" + + assert 0 == fsw.render(self.ts(minute=1, second=20, microsecond=0)), \ + "F. First second one minute after all record() calls"