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: if len(self.history) >= self.num_requests:
raise _503_THROTTLED_RESPONSE self.throttle_failure()
else:
history.insert(0, now) self.throttle_success()
cache.set(key, history, duration)
def throttle_success(self):
class PerResourceThrottling(BasePermission): """Inserts the current request's timesatmp along with the key into the cache."""
self.history.insert(0, self.now)
cache.set(self.key, self.history, self.duration)
def throttle_failure(self):
"""Raises a 503 """
raise _503_THROTTLED_RESPONSE
class PerUserThrottling(BaseThrottle):
""" """
Rate throttling of requests on a per-resource basis.
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 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, [])
now = time.time()
# Drop any requests from the history which have now passed the throttle duration
while history and history[0] < now - duration:
history.pop()
if len(history) >= num_requests: class PerViewThrottling(BaseThrottle):
raise _503_THROTTLED_RESPONSE """
The class name of the cuurent view will be used as a unique identifier.
"""
def get_cache_key(self):
return 'throttle_%s' % self.view.__class__.__name__
class PerResourceThrottling(BaseThrottle):
"""
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