heartbeat and timeout usable as context managers

This commit is contained in:
scottsanderson
2012-08-19 02:21:43 -04:00
parent 0ef89ee704
commit b5cb08ceed
+60 -37
View File
@@ -1,84 +1,109 @@
import signal import signal
from functools import wraps
from pprint import pprint as pp from pprint import pprint as pp
from numbers import Number from numbers import Number
from logbook import Logger from logbook import Logger
class Timeout(Exception): class Timeout(Exception):
def __init__(self, frame): def __init__(self, frame, message=''):
self.frame = frame self.frame = frame
self.message = message
class timeout(object): class timeout(object):
""" """
Decorator to make a function raise TimeoutException if it spends Utility to make a function raise TimeoutException if it spends
more than a specified number of seconds executing. more than a specified number of seconds executing. Can be used
as a decorator to apply a static timeout to a function, or as
a context manager to dynamically add a timeout to a code block.
""" """
def __init__(self, seconds): def __init__(self, seconds, message=''):
self.seconds = seconds self.seconds = seconds
self.message = message
assert isinstance(seconds, Number), "Failed to specify a timeout." assert isinstance(seconds, Number), "Failed to specify a timeout."
assert seconds > 0, "Timeout must be greater than 0" assert seconds > 0, "Timeout must be greater than 0"
def handler(self, signum, frame): def handler(self, signum, frame):
raise Timeout(frame) raise Timeout(frame, self.message)
def __call__(self, fn): def __call__(self, fn):
def wrapped(*args, **kwargs): @wraps(fn)
def call_fn_with_timeout(*args, **kwargs):
# Set the alarm. # Set the alarm.
signal.signal(signal.SIGALRM, self.handler) signal.signal(signal.SIGALRM, self.handler)
signal.alarm(self.seconds) signal.setitimer(signal.ITIMER_REAL, self.seconds, 0)
try: try:
outval = fn(*args, **kwargs) outval = fn(*args, **kwargs)
# Deactivate the alarm once we're done so that the # Deactivate the alarm once we're done so that the
# decorator doesn't have unexpected side-effects later. # decorator doesn't have unexpected side-effects later.
# Note that this will still raise TimeoutException if the # Note that this will still raise Timeout if the
# call to fn takes too long. # call to fn takes too long.
finally: finally:
signal.alarm(0) signal.setitimer(signal.ITIMER_REAL, 0, 0)
signal.signal(signal.SIGALRM, signal.SIG_DFL) signal.signal(signal.SIGALRM, signal.SIG_DFL)
# Return the value of fn if it finished before the alarm. This # Return the value of fn if it finished before the alarm. This
# won't execute if the Timeout was raised. # won't execute if the Timeout was raised.
return outval return outval
return wrapped return call_fn_with_timeout
def __enter__(self):
# Set the alarm on entrance.
signal.signal(signal.SIGALRM, self.handler)
signal.setitimer(signal.ITIMER_REAL, self.seconds, 0)
def __exit__(self, type, value, traceback):
# Deactivate the alarm on exit. This will re-raise
# any exceptions raised inside the with block.
signal.signal(signal.SIGALRM, self.handler)
signal.setitimer(signal.ITIMER_REAL, self.seconds, 0)
class heartbeat(object): class heartbeat(object):
""" """
Decorator to perform pseudo-heartbeat checks on a single-threaded Utility to perform pseudo-heartbeat checks on a single-threaded
function. Calls frame_handler on the current stack frame of the function. Calls frame_handler on the current stack frame of the
decorated function every ``interval`` seconds. After ``max_interval`` wrapped function every ``interval`` seconds. After ``max_interval``
intervals, raises MaxHeartBeats intervals, raises Timeout. Can be used either as a decorator or
a context manager.
""" """
def __init__(self,
interval,
max_intervals,
frame_handler=None,
timeout_message=''):
def __init__(self, interval, max_intervals, frame_handler=None):
self.count = 0
self.interval = interval self.interval = interval
self.max_intervals = max_intervals self.max_intervals = max_intervals
self.frame_handler = frame_handler self.frame_handler = frame_handler
self.timeout_message = timeout_message
self.count = 0
def handler(self, signum, frame): def handler(self, signum, frame):
self.count += 1 self.count += 1
if self.frame_handler: if self.frame_handler:
self.frame_handler(frame) self.frame_handler(self.count, frame)
if self.count > self.max_intervals: if self.count >= self.max_intervals:
raise Timeout(frame) raise Timeout(frame, self.timeout_message)
def __call__(self, fn): def __call__(self, fn):
def wrapped(*args, **kwargs):
# Set a timer to call our handler every N seconds. @wraps(fn)
def call_fn_with_heartbeat(*args, **kwargs):
# Set a timer to call our handler every ``interval`` seconds.
signal.signal(signal.SIGALRM, self.handler) signal.signal(signal.SIGALRM, self.handler)
signal.setitimer(signal.ITIMER_REAL, self.interval, self.interval) signal.setitimer(signal.ITIMER_REAL, self.interval, self.interval)
try: try:
outval = fn(*args, **kwargs) outval = fn(*args, **kwargs)
# Deactivate the timer once we're done so that the
# decorator doesn't have unexpected side-effects later.
finally: finally:
# Deactivate the timer once we're done so that the
# decorator doesn't have unexpected side-effects later.
signal.setitimer(signal.ITIMER_REAL, 0, 0) signal.setitimer(signal.ITIMER_REAL, 0, 0)
signal.signal(signal.SIGALRM, signal.SIG_DFL) signal.signal(signal.SIGALRM, signal.SIG_DFL)
@@ -86,17 +111,15 @@ class heartbeat(object):
# an exception. This won't execute if the Timeout or any # an exception. This won't execute if the Timeout or any
# other exception was raised by self.handle. # other exception was raised by self.handle.
return outval return outval
return wrapped return call_fn_with_heartbeat
if __name__ == "__main__":
import time
def pframe_g(frame): def __enter__(self):
print frame.f_globals # Set a timer to call our handler every N seconds.
signal.signal(signal.SIGALRM, self.handler)
@heartbeat(1, 10, pframe_g) signal.setitimer(signal.ITIMER_REAL, self.interval, self.interval)
def foo():
for i in xrange(10000): def __exit__(self, type, value, traceback):
time.sleep(.1) # Turn off the timer on exit. This will re-raise any exception raised
print i # during execution of the with-block
foo() signal.setitimer(signal.ITIMER_REAL, 0, 0)
signal.signal(signal.SIGALRM, signal.SIG_DFL)