Commit 5be359fb by markotibold

* implemented Tom's nice config string for the trotlle rate e.g. '3/sec'

* We now have per-user, per-view and per-resource throttling

* Added a new exxception class as a convenience to detect pointless throttles

* refactored
parent f0b3b9d7
...@@ -29,6 +29,10 @@ _503_THROTTLED_RESPONSE = ErrorResponse( ...@@ -29,6 +29,10 @@ _503_THROTTLED_RESPONSE = ErrorResponse(
{'detail': 'request was throttled'}) {'detail': 'request was throttled'})
class ConfigurationException(BaseException):
"""To alert for bad configuration desicions as a convenience."""
pass
class BasePermission(object): class BasePermission(object):
""" """
...@@ -87,70 +91,83 @@ class IsUserOrIsAnonReadOnly(BasePermission): ...@@ -87,70 +91,83 @@ class IsUserOrIsAnonReadOnly(BasePermission):
self.view.method != 'HEAD'): self.view.method != 'HEAD'):
raise _403_FORBIDDEN_RESPONSE raise _403_FORBIDDEN_RESPONSE
class BaseThrottle(BasePermission):
class PerUserThrottling(BasePermission):
""" """
Rate throttling of requests on a per-user basis. Rate throttling of requests.
The rate (requests / seconds) is set by a :attr:`throttle` attribute on the ``View`` class. The rate (requests / seconds) is set by a :attr:`throttle` attribute on the ``View`` class.
The attribute is a two tuple of the form (number of requests, duration in seconds). The attribute is a string of the form 'number of requests/period'. Period must be an element
of (sec, min, hour, day)
The user id will be used as a unique identifier if the user is authenticated.
For anonymous requests, the IP address of the client will be used.
Previous request information used for throttling is stored in the cache. Previous request information used for throttling is stored in the cache.
""" """
def check_permission(self, user): def get_cache_key(self):
(num_requests, duration) = getattr(self.view, 'throttle', (0, 0)) """Should return the cache-key corresponding to the semantics of the class that implements
the throttling behaviour.
if user.is_authenticated(): """
ident = str(user) pass
else:
ident = self.view.request.META.get('REMOTE_ADDR', None)
key = 'throttle_%s' % ident def check_permission(self, auth):
history = cache.get(key, []) num, period = getattr(self.view, 'throttle', '0/sec').split('/')
now = time.time() self.num_requests = int(num)
self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
self.auth = auth
self.check_throttle()
def check_throttle(self):
"""On success calls `throttle_success`. On failure calls `throttle_failure`. """
self.key = self.get_cache_key()
self.history = cache.get(self.key, [])
self.now = time.time()
# Drop any requests from the history which have now passed the throttle duration # Drop any requests from the history which have now passed the throttle duration
while history and history[0] < now - duration: while self.history and self.history[0] < self.now - self.duration:
history.pop() self.history.pop()
if len(history) >= num_requests:
raise _503_THROTTLED_RESPONSE
history.insert(0, now) if len(self.history) >= self.num_requests:
cache.set(key, history, duration) self.throttle_failure()
else:
self.throttle_success()
class PerResourceThrottling(BasePermission): def throttle_success(self):
""" """Inserts the current request's timesatmp along with the key into the cache."""
Rate throttling of requests on a per-resource basis. self.history.insert(0, self.now)
cache.set(self.key, self.history, self.duration)
The rate (requests / seconds) is set by a :attr:`throttle` attribute on the ``View`` class. def throttle_failure(self):
The attribute is a two tuple of the form (number of requests, duration in seconds). """Raises a 503 """
raise _503_THROTTLED_RESPONSE
class PerUserThrottling(BaseThrottle):
"""
The user id will be used as a unique identifier if the user is authenticated. The user id will be used as a unique identifier if the user is authenticated.
For anonymous requests, the IP address of the client will be used. For anonymous requests, the IP address of the client will be used.
Previous request information used for throttling is stored in the cache.
""" """
def check_permission(self, ignore): def get_cache_key(self):
(num_requests, duration) = getattr(self.view, 'throttle', (0, 0)) if self.auth.is_authenticated():
ident = str(self.auth)
else:
key = 'throttle_%s' % self.view.__class__.__name__ ident = self.view.request.META.get('REMOTE_ADDR', None)
return 'throttle_%s' % ident
history = cache.get(key, []) class PerViewThrottling(BaseThrottle):
now = time.time() """
The class name of the cuurent view will be used as a unique identifier.
"""
# Drop any requests from the history which have now passed the throttle duration def get_cache_key(self):
while history and history[0] < now - duration: return 'throttle_%s' % self.view.__class__.__name__
history.pop()
if len(history) >= num_requests: class PerResourceThrottling(BaseThrottle):
raise _503_THROTTLED_RESPONSE """
The class name of the cuurent resource will be used as a unique identifier.
Raises :exc:`ConfigurationException` if no resource attribute is set on the view class.
"""
history.insert(0, now) def get_cache_key(self):
cache.set(key, history, duration) if self.view.resource != None:
return 'throttle_%s' % self.view.resource.__class__.__name__
raise ConfigurationException(
"A per-resource throttle was set to a view that does not have a resource.")
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment