| # | |
| # Licensed to the Apache Software Foundation (ASF) under one or more | |
| # contributor license agreements. See the NOTICE file distributed with | |
| # this work for additional information regarding copyright ownership. | |
| # The ASF licenses this file to You under the Apache License, Version 2.0 | |
| # (the "License"); you may not use this file except in compliance with | |
| # the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # | |
| # Utility functions & classes that are _not_ specific to the datastore client. | |
| # | |
| # For internal use only; no backwards-compatibility guarantees. | |
| # pytype: skip-file | |
| import math | |
| class MovingSum(object): | |
| """Class that keeps track of a rolling window sum. | |
| For use in tracking recent performance of the connector. | |
| Intended to be similar to | |
| org.apache.beam.sdk.util.MovingFunction(..., Sum.ofLongs()), but for | |
| convenience we expose the count of entries as well so this doubles as a | |
| moving average tracker. | |
| """ | |
| def __init__(self, window_ms, bucket_ms): | |
| if window_ms < bucket_ms or bucket_ms <= 0: | |
| raise ValueError("window_ms >= bucket_ms > 0 please") | |
| self._num_buckets = int(math.ceil(window_ms / bucket_ms)) | |
| self._bucket_ms = bucket_ms | |
| self._Reset(now=0) # initialize the moving window members | |
| def _Reset(self, now): | |
| self._current_index = 0 # pointer into self._buckets | |
| self._current_ms_since_epoch = math.floor( | |
| now / self._bucket_ms) * self._bucket_ms | |
| # _buckets is a list where each element is a list [sum, num_samples] | |
| # This is a circular buffer where | |
| # [_current_index] represents the time range | |
| # [_current_ms_since_epoch, _current_ms_since_epoch+_bucket_ms) | |
| # [_current_index-1] represents immediatly prior time range | |
| # [_current_ms_since_epoch-_bucket_ms, _current_ms_since_epoch) | |
| # etc, wrapping around from the start to the end of the array, so | |
| # [_current_index+1] is the element representing the oldest bucket. | |
| self._buckets = [[0, 0] for _ in range(0, self._num_buckets)] | |
| def _Flush(self, now): | |
| """ | |
| Args: | |
| now: int, milliseconds since epoch | |
| """ | |
| if now >= (self._current_ms_since_epoch + | |
| self._bucket_ms * self._num_buckets): | |
| # Time moved forward so far that all currently held data is outside of | |
| # the window. It is faster to simply reset our data. | |
| self._Reset(now) | |
| return | |
| while now > self._current_ms_since_epoch + self._bucket_ms: | |
| # Advance time by one _bucket_ms, setting the new bucket's counts to 0. | |
| self._current_ms_since_epoch += self._bucket_ms | |
| self._current_index = (self._current_index + 1) % self._num_buckets | |
| self._buckets[self._current_index] = [0, 0] | |
| # Intentional dead reckoning here; we don't care about staying precisely | |
| # aligned with multiples of _bucket_ms since the epoch, we just need our | |
| # buckets to represent the most recent _window_ms time window. | |
| def sum(self, now): | |
| self._Flush(now) | |
| return sum(bucket[0] for bucket in self._buckets) | |
| def add(self, now, inc): | |
| self._Flush(now) | |
| bucket = self._buckets[self._current_index] | |
| bucket[0] += inc | |
| bucket[1] += 1 | |
| def count(self, now): | |
| self._Flush(now) | |
| return sum(bucket[1] for bucket in self._buckets) | |
| def has_data(self, now): | |
| return self.count(now) > 0 |