diff options
Diffstat (limited to 'pandora/ratelimit.py')
-rw-r--r-- | pandora/ratelimit.py | 164 |
1 files changed, 164 insertions, 0 deletions
diff --git a/pandora/ratelimit.py b/pandora/ratelimit.py new file mode 100644 index 0000000..427ede9 --- /dev/null +++ b/pandora/ratelimit.py | |||
@@ -0,0 +1,164 @@ | |||
1 | """ | ||
2 | Pandora Rate Limiter | ||
3 | """ | ||
4 | import time | ||
5 | import warnings | ||
6 | |||
7 | from .errors import PandoraException | ||
8 | |||
9 | |||
10 | class RateLimitExceeded(PandoraException): | ||
11 | """Exception thrown when rate limit is exceeded | ||
12 | """ | ||
13 | |||
14 | code = 0 | ||
15 | message = "Rate Limit Exceeded" | ||
16 | |||
17 | |||
18 | class TokenBucketCallbacks(object): | ||
19 | """Interface for TokenBucket Callbacks | ||
20 | |||
21 | These methods get called by the token bucket during certain parts of the | ||
22 | bucket lifecycle. | ||
23 | """ | ||
24 | |||
25 | def near_depletion(self, tokens_left): | ||
26 | """Bucket near depletion callback | ||
27 | |||
28 | This callback is called when the token bucket is nearly depleted, as | ||
29 | defined by the depletion_level member of the TokenBucket class. This | ||
30 | method provides a hook for clients to warn when the token bucket is | ||
31 | nearly consumed. The return value of this method is ignored. | ||
32 | """ | ||
33 | return | ||
34 | |||
35 | def depleted(self, tokens_wanted, tokens_left): | ||
36 | """Bucket depletion callback | ||
37 | |||
38 | This callback is called when the token bucket has been depleted. | ||
39 | Returns a boolean indicating if the bucket should call its default | ||
40 | depletion handler. If this method returns False the token bucket will | ||
41 | not return any tokens but will also not call the default token | ||
42 | depletion handler. | ||
43 | |||
44 | This method is useful if clients want to customize depletion behavior. | ||
45 | """ | ||
46 | return True | ||
47 | |||
48 | def consumed(self, tokens_wanted, tokens_left): | ||
49 | """Bucket consumption callback | ||
50 | |||
51 | This callback is called when a client successfully consumes tokens from | ||
52 | a bucket. The return value of this method is ignored. | ||
53 | """ | ||
54 | return | ||
55 | |||
56 | |||
57 | class TokenBucket(object): | ||
58 | def __init__(self, capacity, refill_tokens, refill_rate, callbacks=None): | ||
59 | """Initialize a rate limiter | ||
60 | |||
61 | capacity | ||
62 | the number of tokens the bucket holds when completely full | ||
63 | refill_tokens | ||
64 | number of tokens to add to the bucket during each refill cycle | ||
65 | refill_rate | ||
66 | the number of seconds between token refills | ||
67 | """ | ||
68 | self.capacity = capacity | ||
69 | self.refill_tokens = refill_tokens | ||
70 | self.refill_rate = refill_rate | ||
71 | self.callbacks = callbacks or TokenBucketCallbacks() | ||
72 | |||
73 | # Depletion level at which the near_depletion callback is called. | ||
74 | # Defaults to 20% of the bucket available. Not exposed in the | ||
75 | # initializer because this should generally be good enough. | ||
76 | self.depletion_level = self.capacity / 5 | ||
77 | |||
78 | self._available_tokens = capacity | ||
79 | self._last_refill = time.time() | ||
80 | |||
81 | @classmethod | ||
82 | def creator(cls, callbacks=None): | ||
83 | """Returns a TokenBucket creator | ||
84 | |||
85 | This method is used when clients want to customize the callbacks but | ||
86 | defer class construction to the real consumer. | ||
87 | """ | ||
88 | |||
89 | def constructor(capacity, refill_tokens, refill_rate): | ||
90 | return cls(capacity, refill_tokens, refill_rate, callbacks) | ||
91 | |||
92 | return constructor | ||
93 | |||
94 | def _replentish(self): | ||
95 | now = time.time() | ||
96 | |||
97 | refill_unit = round((now - self._last_refill) / self.refill_rate) | ||
98 | if refill_unit > 0: | ||
99 | self._last_refill = now | ||
100 | self._available_tokens = min( | ||
101 | self.capacity, refill_unit * self.refill_tokens | ||
102 | ) | ||
103 | |||
104 | def _insufficient_tokens(self, tokens_wanted): | ||
105 | return | ||
106 | |||
107 | def _sufficient_tokens(self, tokens_wanted): | ||
108 | return | ||
109 | |||
110 | def consume(self, tokens=1): | ||
111 | """Consume a number of tokens from the bucket | ||
112 | |||
113 | May return a boolean indicating that sufficient tokens were available | ||
114 | for consumption. Implementations may have different behaviour when the | ||
115 | bucket is empty. This method may block or throw. | ||
116 | """ | ||
117 | self._replentish() | ||
118 | |||
119 | if self._available_tokens >= tokens: | ||
120 | self._available_tokens -= tokens | ||
121 | |||
122 | if self._available_tokens <= self.depletion_level: | ||
123 | self.callbacks.near_depletion(self._available_tokens) | ||
124 | |||
125 | self.callbacks.consumed(tokens, self._available_tokens) | ||
126 | self._sufficient_tokens(tokens) | ||
127 | |||
128 | return True | ||
129 | else: | ||
130 | if self.callbacks.depleted(tokens, self._available_tokens): | ||
131 | self._insufficient_tokens(tokens) | ||
132 | return False | ||
133 | |||
134 | |||
135 | class BlockingTokenBucket(TokenBucket): | ||
136 | """Token bucket that blocks on exhaustion | ||
137 | """ | ||
138 | |||
139 | def _insufficient_tokens(self, tokens_wanted): | ||
140 | tokens_per_second = self.refill_rate / self.refill_tokens | ||
141 | excess_tokens = tokens_wanted - self._available_tokens | ||
142 | time.sleep(tokens_per_second * excess_tokens) | ||
143 | |||
144 | |||
145 | class ThrowingTokenBucket(TokenBucket): | ||
146 | """Token bucket that throws on exhaustion | ||
147 | """ | ||
148 | |||
149 | def _insufficient_tokens(self, tokens_wanted): | ||
150 | raise RateLimitExceeded("Unable to acquire enough tokens") | ||
151 | |||
152 | |||
153 | class WarningTokenBucket(TokenBucket): | ||
154 | """Token bucket that warns on exhaustion | ||
155 | |||
156 | This token bucket doesn't enforce the rate limit and is designed to be a | ||
157 | softer policy for backwards compatability with code that was written before | ||
158 | rate limiting was added. | ||
159 | """ | ||
160 | |||
161 | def _insufficient_tokens(self, tokens_wanted): | ||
162 | warnings.warn( | ||
163 | "Pandora API rate limit exceeded!", warnings.ResourceWarning | ||
164 | ) | ||