Files
deb-python-taskflow/taskflow/types/timing.py
Joshua Harlow d5128cf51a Stopwatch usage cleanup/tweak
Instead of optionally creating a stopwatch when a provided
timeout is not none (to avoid the stopwatch leftover() method
raising a error) just allow the stopwatch leftover() method to
not raise when no duration is provided to avoid these repeated
styles of usage/checks in the first place.

By default the leftover() method still raises an error (a new
keyword argument is now accepted to turn off this behavior).

Change-Id: If934ee6e6855adbb6975cd6ea41e273d40e73dac
2015-01-28 16:14:54 -08:00

297 lines
9.7 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved.
#
# Licensed 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.
from oslo_utils import reflection
from taskflow.utils import misc
from taskflow.utils import threading_utils
# Find a monotonic providing time (or fallback to using time.time()
# which isn't *always* accurate but will suffice).
_now = misc.find_monotonic(allow_time_time=True)
class Timeout(object):
"""An object which represents a timeout.
This object has the ability to be interrupted before the actual timeout
is reached.
"""
def __init__(self, timeout):
if timeout < 0:
raise ValueError("Timeout must be >= 0 and not %s" % (timeout))
self._timeout = timeout
self._event = threading_utils.Event()
def interrupt(self):
self._event.set()
def is_stopped(self):
return self._event.is_set()
def wait(self):
self._event.wait(self._timeout)
def reset(self):
self._event.clear()
class Split(object):
"""A *immutable* stopwatch split.
See: http://en.wikipedia.org/wiki/Stopwatch for what this is/represents.
"""
__slots__ = ['_elapsed', '_length']
def __init__(self, elapsed, length):
self._elapsed = elapsed
self._length = length
@property
def elapsed(self):
"""Duration from stopwatch start."""
return self._elapsed
@property
def length(self):
"""Seconds from last split (or the elapsed time if no prior split)."""
return self._length
def __repr__(self):
r = reflection.get_class_name(self, fully_qualified=False)
r += "(elapsed=%s, length=%s)" % (self._elapsed, self._length)
return r
class StopWatch(object):
"""A simple timer/stopwatch helper class.
Inspired by: apache-commons-lang java stopwatch.
Not thread-safe (when a single watch is mutated by multiple threads at
the same time). Thread-safe when used by a single thread (not shared) or
when operations are performed in a thread-safe manner on these objects by
wrapping those operations with locks.
"""
_STARTED = 'STARTED'
_STOPPED = 'STOPPED'
"""
Class variables that should only be used for testing purposes only...
"""
_now_offset = None
_now_override = None
def __init__(self, duration=None):
if duration is not None:
if duration < 0:
raise ValueError("Duration must be >= 0 and not %s" % duration)
self._duration = duration
else:
self._duration = None
self._started_at = None
self._stopped_at = None
self._state = None
self._splits = []
def start(self):
"""Starts the watch (if not already started).
NOTE(harlowja): resets any splits previously captured (if any).
"""
if self._state == self._STARTED:
return self
self._started_at = self._now()
self._stopped_at = None
self._state = self._STARTED
self._splits = []
return self
@property
def splits(self):
"""Accessor to all/any splits that have been captured."""
return tuple(self._splits)
def split(self):
"""Captures a split/elapsed since start time (and doesn't stop)."""
if self._state == self._STARTED:
elapsed = self.elapsed()
if self._splits:
length = self._delta_seconds(self._splits[-1].elapsed, elapsed)
else:
length = elapsed
self._splits.append(Split(elapsed, length))
return self._splits[-1]
else:
raise RuntimeError("Can not create a split time of a stopwatch"
" if it has not been started")
def restart(self):
"""Restarts the watch from a started/stopped state."""
if self._state == self._STARTED:
self.stop()
self.start()
return self
@classmethod
def clear_overrides(cls):
"""Clears all overrides/offsets.
**Only to be used for testing (affects all watch instances).**
"""
cls._now_override = None
cls._now_offset = None
@classmethod
def set_offset_override(cls, offset):
"""Sets a offset that is applied to each time fetch.
**Only to be used for testing (affects all watch instances).**
"""
cls._now_offset = offset
@classmethod
def advance_time_seconds(cls, offset):
"""Advances/sets a offset that is applied to each time fetch.
NOTE(harlowja): if a previous offset exists (not ``None``) then this
offset will be added onto the existing one (if you want to reset
the offset completely use the :meth:`.set_offset_override`
method instead).
**Only to be used for testing (affects all watch instances).**
"""
if cls._now_offset is None:
cls.set_offset_override(offset)
else:
cls.set_offset_override(cls._now_offset + offset)
@classmethod
def set_now_override(cls, now=None):
"""Sets time override to use (if none, then current time is fetched).
NOTE(harlowja): if a list/tuple is provided then the first element of
the list will be used (and removed) each time a time fetch occurs (once
it becomes empty the override/s will no longer be applied). If a
numeric value is provided then it will be used (and never removed
until the override(s) are cleared via the :meth:`.clear_overrides`
method).
**Only to be used for testing (affects all watch instances).**
"""
if isinstance(now, (list, tuple)):
cls._now_override = list(now)
else:
if now is None:
now = _now()
cls._now_override = now
@staticmethod
def _delta_seconds(earlier, later):
return max(0.0, later - earlier)
@classmethod
def _now(cls):
if cls._now_override is not None:
if isinstance(cls._now_override, list):
try:
now = cls._now_override.pop(0)
except IndexError:
now = _now()
else:
now = cls._now_override
else:
now = _now()
if cls._now_offset is not None:
now = now + cls._now_offset
return now
def elapsed(self, maximum=None):
"""Returns how many seconds have elapsed."""
if self._state not in (self._STOPPED, self._STARTED):
raise RuntimeError("Can not get the elapsed time of a stopwatch"
" if it has not been started/stopped")
if self._state == self._STOPPED:
elapsed = self._delta_seconds(self._started_at, self._stopped_at)
else:
elapsed = self._delta_seconds(self._started_at, self._now())
if maximum is not None and elapsed > maximum:
elapsed = max(0.0, maximum)
return elapsed
def __enter__(self):
"""Starts the watch."""
self.start()
return self
def __exit__(self, type, value, traceback):
"""Stops the watch (ignoring errors if stop fails)."""
try:
self.stop()
except RuntimeError:
pass
def leftover(self, return_none=False):
"""Returns how many seconds are left until the watch expires.
:param return_none: when ``True`` instead of raising a ``RuntimeError``
when no duration has been set this call will
return ``None`` instead.
:type return_none: boolean
"""
if self._state != self._STARTED:
raise RuntimeError("Can not get the leftover time of a stopwatch"
" that has not been started")
if self._duration is None:
if not return_none:
raise RuntimeError("Can not get the leftover time of a watch"
" that has no duration")
else:
return None
return max(0.0, self._duration - self.elapsed())
def expired(self):
"""Returns if the watch has expired (ie, duration provided elapsed)."""
if self._state is None:
raise RuntimeError("Can not check if a stopwatch has expired"
" if it has not been started/stopped")
if self._duration is None:
return False
if self.elapsed() > self._duration:
return True
return False
def resume(self):
"""Resumes the watch from a stopped state."""
if self._state == self._STOPPED:
self._state = self._STARTED
return self
else:
raise RuntimeError("Can not resume a stopwatch that has not been"
" stopped")
def stop(self):
"""Stops the watch."""
if self._state == self._STOPPED:
return self
if self._state != self._STARTED:
raise RuntimeError("Can not stop a stopwatch that has not been"
" started")
self._stopped_at = self._now()
self._state = self._STOPPED
return self