Skip to content

Commit b6691ac

Browse files
committed
Added throttle() as simpler alternative to LocalThrottle
1 parent aa4cfde commit b6691ac

1 file changed

Lines changed: 109 additions & 12 deletions

File tree

src/bd2k/util/throttle.py

Lines changed: 109 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import time
44
import threading
5+
56
from bd2k.util.threading import BoundedEmptySemaphore
67

78

@@ -16,23 +17,23 @@ class GlobalThrottle:
1617
prevent the resource from becoming swamped after longer pauses.
1718
"""
1819

19-
def __init__(self, min_interval, max_unused):
20+
def __init__( self, min_interval, max_unused ):
2021
self.min_interval = min_interval
2122
self.semaphore = BoundedEmptySemaphore( max_unused )
2223
self.thread_start_lock = threading.Lock( )
2324
self.thread_started = False
2425
self.thread = threading.Thread( target=self.generator )
2526
self.thread.daemon = True
2627

27-
def generator(self):
28+
def generator( self ):
2829
while True:
2930
try:
3031
self.semaphore.release( )
3132
except ValueError:
3233
pass
3334
time.sleep( self.min_interval )
3435

35-
def throttle(self, wait=True):
36+
def throttle( self, wait=True ):
3637
"""
3738
If the wait parameter is True, this method returns True after suspending the current
3839
thread as necessary to ensure that no less than the configured minimum interval passed
@@ -49,8 +50,8 @@ def throttle(self, wait=True):
4950
self.thread_started = True
5051
return self.semaphore.acquire( blocking=wait )
5152

52-
def __call__(self, function):
53-
def wrapper(*args, **kwargs):
53+
def __call__( self, function ):
54+
def wrapper( *args, **kwargs ):
5455
self.throttle( )
5556
return function( *args, **kwargs )
5657

@@ -60,10 +61,12 @@ def wrapper(*args, **kwargs):
6061
class LocalThrottle:
6162
"""
6263
A thread-safe rate limiter that throttles each thread independently. Can be used as a
63-
function or method decorator or as a simple object, using the throttle().
64+
function or method decorator or as a simple object, via its .throttle() method.
65+
66+
The use as a decorator is deprecated in favor of throttle().
6467
"""
6568

66-
def __init__(self, min_interval):
69+
def __init__( self, min_interval ):
6770
"""
6871
Initialize this local throttle.
6972
@@ -74,7 +77,7 @@ def __init__(self, min_interval):
7477
self.per_thread = threading.local( )
7578
self.per_thread.last_invocation = None
7679

77-
def throttle(self, wait=True):
80+
def throttle( self, wait=True ):
7881
"""
7982
If the wait parameter is True, this method returns True after suspending the current
8083
thread as necessary to ensure that no less than the configured minimum interval has
@@ -97,10 +100,104 @@ def throttle(self, wait=True):
97100
self.per_thread.last_invocation = now
98101
return True
99102

100-
101-
def __call__(self, function):
102-
def wrapper(*args, **kwargs):
103+
def __call__( self, function ):
104+
def wrapper( *args, **kwargs ):
103105
self.throttle( )
104106
return function( *args, **kwargs )
105107

106-
return wrapper
108+
return wrapper
109+
110+
111+
class throttle( object ):
112+
"""
113+
A context manager for ensuring that the execution of its body takes at least a given amount
114+
of time, sleeping if necessary. It is a simpler version of LocalThrottle if used as a
115+
decorator.
116+
117+
Ensures that body takes at least the given amount of time.
118+
119+
>>> start = time.time()
120+
>>> with throttle(1):
121+
... pass
122+
>>> 1 <= time.time() - start <= 1.1
123+
True
124+
125+
Ditto when used as a decorator.
126+
127+
>>> @throttle(1)
128+
... def f():
129+
... pass
130+
>>> start = time.time()
131+
>>> f()
132+
>>> 1 <= time.time() - start <= 1.1
133+
True
134+
135+
If the body takes longer by itself, don't throttle.
136+
137+
>>> start = time.time()
138+
>>> with throttle(1):
139+
... time.sleep(2)
140+
>>> 2 <= time.time() - start <= 2.1
141+
True
142+
143+
Ditto when used as a decorator.
144+
145+
>>> @throttle(1)
146+
... def f():
147+
... time.sleep(2)
148+
>>> start = time.time()
149+
>>> f()
150+
>>> 2 <= time.time() - start <= 2.1
151+
True
152+
153+
If an exception occurs, don't throttle.
154+
155+
>>> start = time.time()
156+
>>> try:
157+
... with throttle(1):
158+
... raise ValueError('foo')
159+
... except ValueError:
160+
... end = time.time()
161+
... raise
162+
Traceback (most recent call last):
163+
...
164+
ValueError: foo
165+
>>> 0 <= end - start <= 0.1
166+
True
167+
168+
Ditto when used as a decorator.
169+
170+
>>> @throttle(1)
171+
... def f():
172+
... raise ValueError('foo')
173+
>>> start = time.time()
174+
>>> try:
175+
... f()
176+
... except ValueError:
177+
... end = time.time()
178+
... raise
179+
Traceback (most recent call last):
180+
...
181+
ValueError: foo
182+
>>> 0 <= end - start <= 0.1
183+
True
184+
"""
185+
186+
def __init__( self, min_interval ):
187+
self.min_interval = min_interval
188+
189+
def __enter__( self ):
190+
self.start = time.time( )
191+
192+
def __exit__( self, exc_type, exc_val, exc_tb ):
193+
if exc_type is None:
194+
duration = time.time( ) - self.start
195+
remainder = self.min_interval - duration
196+
if remainder > 0:
197+
time.sleep( remainder )
198+
199+
def __call__( self, function ):
200+
def wrapper( *args, **kwargs ):
201+
with self:
202+
return function( *args, **kwargs )
203+
return wrapper

0 commit comments

Comments
 (0)