Pulled out Rate Limit middleware
Rate Limit middleware is now at http://dpgoetz.github.com/swift-ratelimit/ For current users of Rate Limit, this will require installing the new package and changing the "use" line of the ratelimit conf section to: [filter:ratelimit] use = egg:swiftratelimit#middleware And then 'swift-init proxy reload'. Change-Id: I2ab774e9cee9fba4103c1be4bea6d52d1adb29f7
This commit is contained in:
parent
7dfbd785b0
commit
3f00c1a630
@ -90,7 +90,7 @@ are acceptable within this section.
|
||||
.IP "\fBpipeline\fR"
|
||||
It is used when you need apply a number of filters. It is a list of filters
|
||||
ended by an application. The default should be \fB"catch_errors healthcheck
|
||||
cache ratelimit tempauth proxy-server"\fR
|
||||
cache tempauth proxy-server"\fR
|
||||
.RE
|
||||
.PD
|
||||
|
||||
@ -209,53 +209,6 @@ Default for memcache_servers is to try to read the property from /etc/swift/memc
|
||||
|
||||
|
||||
|
||||
.RS 0
|
||||
.IP "\fB[filter:ratelimit]\fR"
|
||||
.RE
|
||||
|
||||
Rate limits requests on both an Account and Container level. Limits are configurable.
|
||||
|
||||
.RS 3
|
||||
.IP \fBuse\fR
|
||||
Entry point for paste.deploy for the ratelimit middleware. This is the reference to the installed python egg.
|
||||
The default is \fBegg:swift#ratelimit\fR.
|
||||
.IP "\fBset log_name\fR"
|
||||
Label used when logging. The default is ratelimit.
|
||||
.IP "\fBset log_facility\fR"
|
||||
Syslog log facility. The default is LOG_LOCAL0.
|
||||
.IP "\fBset log_level\fR "
|
||||
Logging level. The default is INFO.
|
||||
.IP "\fBset log_headers\fR "
|
||||
Enables the ability to log request headers. The default is False.
|
||||
.IP \fBclock_accuracy\fR
|
||||
This should represent how accurate the proxy servers' system clocks are with each other.
|
||||
1000 means that all the proxies' clock are accurate to each other within 1 millisecond.
|
||||
No ratelimit should be higher than the clock accuracy. The default is 1000.
|
||||
.IP \fBmax_sleep_time_seconds\fR
|
||||
App will immediately return a 498 response if the necessary sleep time ever exceeds
|
||||
the given max_sleep_time_seconds. The default is 60 seconds.
|
||||
.IP \fBlog_sleep_time_seconds\fR
|
||||
To allow visibility into rate limiting set this value > 0 and all sleeps greater than
|
||||
the number will be logged. If set to 0 means disabled. The default is 0.
|
||||
.IP \fBrate_buffer_seconds\fR
|
||||
Number of seconds the rate counter can drop and be allowed to catch up
|
||||
(at a faster than listed rate). A larger number will result in larger spikes in
|
||||
rate but better average accuracy. The default is 5.
|
||||
.IP \fBaccount_ratelimit\fR
|
||||
If set, will limit PUT and DELETE requests to /account_name/container_name. Number is
|
||||
in requests per second. If set to 0 means disabled. The default is 0.
|
||||
.IP \fBaccount_whitelist\fR
|
||||
Comma separated lists of account names that will not be rate limited. The default is ''.
|
||||
.IP \fBaccount_blacklist\fR
|
||||
Comma separated lists of account names that will not be allowed. Returns a 497 response.
|
||||
The default is ''.
|
||||
.IP \fBcontainer_ratelimit_size\fR
|
||||
When set with container_limit_x = r: for containers of size x, limit requests per second
|
||||
to r. Will limit PUT, DELETE, and POST requests to /a/c/o. The default is ''.
|
||||
.RE
|
||||
|
||||
|
||||
|
||||
.RS 0
|
||||
.IP "\fB[filter:catch_errors]\fR"
|
||||
.RE
|
||||
|
@ -52,7 +52,8 @@ Content Distribution Network Integration
|
||||
Other
|
||||
-----
|
||||
|
||||
* `Domain Remap <https://github.com/notmyname/swift-domainremap>`_ - Translates subdomains on the Host header to path elements that are appropriate for swift.
|
||||
* `Glance <https://github.com/openstack/glance>`_ - Provides services for discovering, registering, and retrieving virtual machine images (for OpenStack Compute [Nova], for example).
|
||||
* `Rate Limit <https://github.com/dpgoetz/swift-ratelimit>`_ - Enforces limits on the request rates to accounts and containers.
|
||||
* `StaticWeb <http://gholt.github.com/swift-staticweb/>`_ - Allows serving static websites from Swift containers using ACLs and other metadata on those containers.
|
||||
* `TempURL/FormPOST <http://gholt.github.com/swift-tempurl/>`_ - Temporary, Expiring URLs and Form POSTing middleware.
|
||||
* `Domain Remap <https://github.com/notmyname/swift-domainremap>`_ - Translates subdomains on the Host header to path elements that are appropriate for swift.
|
||||
|
@ -47,7 +47,6 @@ Overview and Concepts
|
||||
overview_reaper
|
||||
overview_auth
|
||||
overview_replication
|
||||
ratelimit
|
||||
overview_large_objects
|
||||
overview_object_versioning
|
||||
overview_container_sync
|
||||
|
@ -133,13 +133,6 @@ Manager
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Ratelimit
|
||||
=========
|
||||
|
||||
.. automodule:: swift.common.middleware.ratelimit
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Swift3
|
||||
======
|
||||
|
||||
|
@ -1,72 +0,0 @@
|
||||
=============
|
||||
Rate Limiting
|
||||
=============
|
||||
|
||||
Rate limiting in swift is implemented as a pluggable middleware. Rate
|
||||
limiting is performed on requests that result in database writes to the
|
||||
account and container sqlite dbs. It uses memcached and is dependent on
|
||||
the proxy servers having highly synchronized time. The rate limits are
|
||||
limited by the accuracy of the proxy server clocks.
|
||||
|
||||
--------------
|
||||
Configuration
|
||||
--------------
|
||||
|
||||
All configuration is optional. If no account or container limits are provided
|
||||
there will be no rate limiting. Configuration available:
|
||||
|
||||
======================== ========= ===========================================
|
||||
Option Default Description
|
||||
------------------------ --------- -------------------------------------------
|
||||
clock_accuracy 1000 Represents how accurate the proxy servers'
|
||||
system clocks are with each other. 1000
|
||||
means that all the proxies' clock are
|
||||
accurate to each other within 1
|
||||
millisecond. No ratelimit should be
|
||||
higher than the clock accuracy.
|
||||
max_sleep_time_seconds 60 App will immediately return a 498 response
|
||||
if the necessary sleep time ever exceeds
|
||||
the given max_sleep_time_seconds.
|
||||
log_sleep_time_seconds 0 To allow visibility into rate limiting set
|
||||
this value > 0 and all sleeps greater than
|
||||
the number will be logged.
|
||||
rate_buffer_seconds 5 Number of seconds the rate counter can
|
||||
drop and be allowed to catch up (at a
|
||||
faster than listed rate). A larger number
|
||||
will result in larger spikes in rate but
|
||||
better average accuracy.
|
||||
account_ratelimit 0 If set, will limit PUT and DELETE requests
|
||||
to /account_name/container_name.
|
||||
Number is in requests per second.
|
||||
account_whitelist '' Comma separated lists of account names that
|
||||
will not be rate limited.
|
||||
account_blacklist '' Comma separated lists of account names that
|
||||
will not be allowed. Returns a 497 response.
|
||||
container_ratelimit_size '' When set with container_limit_x = r:
|
||||
for containers of size x, limit requests
|
||||
per second to r. Will limit PUT, DELETE,
|
||||
and POST requests to /a/c/o.
|
||||
======================== ========= ===========================================
|
||||
|
||||
The container rate limits are linearly interpolated from the values given. A
|
||||
sample container rate limiting could be:
|
||||
|
||||
container_ratelimit_100 = 100
|
||||
|
||||
container_ratelimit_200 = 50
|
||||
|
||||
container_ratelimit_500 = 20
|
||||
|
||||
This would result in
|
||||
|
||||
================ ============
|
||||
Container Size Rate Limit
|
||||
---------------- ------------
|
||||
0-99 No limiting
|
||||
100 100
|
||||
150 75
|
||||
500 20
|
||||
1000 20
|
||||
================ ============
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
# log_level = INFO
|
||||
|
||||
[pipeline:main]
|
||||
pipeline = catch_errors healthcheck cache ratelimit tempauth proxy-server
|
||||
pipeline = catch_errors healthcheck cache tempauth proxy-server
|
||||
|
||||
[app:proxy-server]
|
||||
use = egg:swift#proxy
|
||||
@ -130,38 +130,6 @@ use = egg:swift#memcache
|
||||
# commas, as in: 10.1.2.3:11211,10.1.2.4:11211
|
||||
# memcache_servers = 127.0.0.1:11211
|
||||
|
||||
[filter:ratelimit]
|
||||
use = egg:swift#ratelimit
|
||||
# You can override the default log routing for this filter here:
|
||||
# set log_name = ratelimit
|
||||
# set log_facility = LOG_LOCAL0
|
||||
# set log_level = INFO
|
||||
# set log_headers = False
|
||||
# clock_accuracy should represent how accurate the proxy servers' system clocks
|
||||
# are with each other. 1000 means that all the proxies' clock are accurate to
|
||||
# each other within 1 millisecond. No ratelimit should be higher than the
|
||||
# clock accuracy.
|
||||
# clock_accuracy = 1000
|
||||
# max_sleep_time_seconds = 60
|
||||
# log_sleep_time_seconds of 0 means disabled
|
||||
# log_sleep_time_seconds = 0
|
||||
# allows for slow rates (e.g. running up to 5 sec's behind) to catch up.
|
||||
# rate_buffer_seconds = 5
|
||||
# account_ratelimit of 0 means disabled
|
||||
# account_ratelimit = 0
|
||||
|
||||
# these are comma separated lists of account names
|
||||
# account_whitelist = a,b
|
||||
# account_blacklist = c,d
|
||||
|
||||
# with container_limit_x = r
|
||||
# for containers of size x limit requests per second to r. The container
|
||||
# rate will be linearly interpolated from the values given. With the values
|
||||
# below, a container of size 5 will get a rate of 75.
|
||||
# container_ratelimit_0 = 100
|
||||
# container_ratelimit_10 = 50
|
||||
# container_ratelimit_50 = 20
|
||||
|
||||
[filter:catch_errors]
|
||||
use = egg:swift#catch_errors
|
||||
# You can override the default log routing for this filter here:
|
||||
|
@ -411,24 +411,6 @@ msgstr ""
|
||||
msgid "Following CNAME chain for %(given_domain)s to %(found_domain)s"
|
||||
msgstr ""
|
||||
|
||||
#: swift/common/middleware/ratelimit.py:172
|
||||
msgid "Returning 497 because of blacklisting"
|
||||
msgstr ""
|
||||
|
||||
#: swift/common/middleware/ratelimit.py:185
|
||||
#, python-format
|
||||
msgid "Ratelimit sleep log: %(sleep)s for %(account)s/%(container)s/%(object)s"
|
||||
msgstr ""
|
||||
|
||||
#: swift/common/middleware/ratelimit.py:192
|
||||
#, python-format
|
||||
msgid "Returning 498 because of ops rate limiting (Max Sleep) %s"
|
||||
msgstr ""
|
||||
|
||||
#: swift/common/middleware/ratelimit.py:212
|
||||
msgid "Warning: Cannot ratelimit without a memcached client"
|
||||
msgstr ""
|
||||
|
||||
#: swift/common/middleware/swauth.py:635
|
||||
#, python-format
|
||||
msgid ""
|
||||
|
1
setup.py
1
setup.py
@ -81,7 +81,6 @@ setup(
|
||||
'paste.filter_factory': [
|
||||
'healthcheck=swift.common.middleware.healthcheck:filter_factory',
|
||||
'memcache=swift.common.middleware.memcache:filter_factory',
|
||||
'ratelimit=swift.common.middleware.ratelimit:filter_factory',
|
||||
'cname_lookup=swift.common.middleware.cname_lookup:filter_factory',
|
||||
'catch_errors=swift.common.middleware.catch_errors:filter_factory',
|
||||
'swift3=swift.common.middleware.swift3:filter_factory',
|
||||
|
@ -1,239 +0,0 @@
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import time
|
||||
import eventlet
|
||||
from webob import Request, Response
|
||||
|
||||
from swift.common.utils import split_path, cache_from_env, get_logger
|
||||
from swift.proxy.server import get_container_memcache_key
|
||||
from swift.common.memcached import MemcacheConnectionError
|
||||
|
||||
|
||||
class MaxSleepTimeHitError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RateLimitMiddleware(object):
|
||||
"""
|
||||
Rate limiting middleware
|
||||
|
||||
Rate limits requests on both an Account and Container level. Limits are
|
||||
configurable.
|
||||
"""
|
||||
|
||||
BLACK_LIST_SLEEP = 1
|
||||
|
||||
def __init__(self, app, conf, logger=None):
|
||||
self.app = app
|
||||
if logger:
|
||||
self.logger = logger
|
||||
else:
|
||||
self.logger = get_logger(conf, log_route='ratelimit')
|
||||
self.account_ratelimit = float(conf.get('account_ratelimit', 0))
|
||||
self.max_sleep_time_seconds = \
|
||||
float(conf.get('max_sleep_time_seconds', 60))
|
||||
self.log_sleep_time_seconds = \
|
||||
float(conf.get('log_sleep_time_seconds', 0))
|
||||
self.clock_accuracy = int(conf.get('clock_accuracy', 1000))
|
||||
self.rate_buffer_seconds = int(conf.get('rate_buffer_seconds', 5))
|
||||
self.ratelimit_whitelist = [acc.strip() for acc in
|
||||
conf.get('account_whitelist', '').split(',') if acc.strip()]
|
||||
self.ratelimit_blacklist = [acc.strip() for acc in
|
||||
conf.get('account_blacklist', '').split(',') if acc.strip()]
|
||||
self.memcache_client = None
|
||||
conf_limits = []
|
||||
for conf_key in conf.keys():
|
||||
if conf_key.startswith('container_ratelimit_'):
|
||||
cont_size = int(conf_key[len('container_ratelimit_'):])
|
||||
rate = float(conf[conf_key])
|
||||
conf_limits.append((cont_size, rate))
|
||||
|
||||
conf_limits.sort()
|
||||
self.container_ratelimits = []
|
||||
while conf_limits:
|
||||
cur_size, cur_rate = conf_limits.pop(0)
|
||||
if conf_limits:
|
||||
next_size, next_rate = conf_limits[0]
|
||||
slope = (float(next_rate) - float(cur_rate)) \
|
||||
/ (next_size - cur_size)
|
||||
|
||||
def new_scope(cur_size, slope, cur_rate):
|
||||
# making new scope for variables
|
||||
return lambda x: (x - cur_size) * slope + cur_rate
|
||||
line_func = new_scope(cur_size, slope, cur_rate)
|
||||
else:
|
||||
line_func = lambda x: cur_rate
|
||||
|
||||
self.container_ratelimits.append((cur_size, cur_rate, line_func))
|
||||
|
||||
def get_container_maxrate(self, container_size):
|
||||
"""
|
||||
Returns number of requests allowed per second for given container size.
|
||||
"""
|
||||
last_func = None
|
||||
if container_size:
|
||||
container_size = int(container_size)
|
||||
for size, rate, func in self.container_ratelimits:
|
||||
if container_size < size:
|
||||
break
|
||||
last_func = func
|
||||
if last_func:
|
||||
return last_func(container_size)
|
||||
return None
|
||||
|
||||
def get_ratelimitable_key_tuples(self, req_method, account_name,
|
||||
container_name=None, obj_name=None):
|
||||
"""
|
||||
Returns a list of key (used in memcache), ratelimit tuples. Keys
|
||||
should be checked in order.
|
||||
|
||||
:param req_method: HTTP method
|
||||
:param account_name: account name from path
|
||||
:param container_name: container name from path
|
||||
:param obj_name: object name from path
|
||||
"""
|
||||
keys = []
|
||||
# COPYs are not limited
|
||||
if self.account_ratelimit and \
|
||||
account_name and container_name and not obj_name and \
|
||||
req_method in ('PUT', 'DELETE'):
|
||||
keys.append(("ratelimit/%s" % account_name,
|
||||
self.account_ratelimit))
|
||||
|
||||
if account_name and container_name and obj_name and \
|
||||
req_method in ('PUT', 'DELETE', 'POST'):
|
||||
container_size = None
|
||||
memcache_key = get_container_memcache_key(account_name,
|
||||
container_name)
|
||||
container_info = self.memcache_client.get(memcache_key)
|
||||
if isinstance(container_info, dict):
|
||||
container_size = container_info.get('container_size', 0)
|
||||
container_rate = self.get_container_maxrate(container_size)
|
||||
if container_rate:
|
||||
keys.append(("ratelimit/%s/%s" % (account_name,
|
||||
container_name),
|
||||
container_rate))
|
||||
return keys
|
||||
|
||||
def _get_sleep_time(self, key, max_rate):
|
||||
'''
|
||||
Returns the amount of time (a float in seconds) that the app
|
||||
should sleep.
|
||||
|
||||
:param key: a memcache key
|
||||
:param max_rate: maximum rate allowed in requests per second
|
||||
:raises: MaxSleepTimeHitError if max sleep time is exceeded.
|
||||
'''
|
||||
try:
|
||||
now_m = int(round(time.time() * self.clock_accuracy))
|
||||
time_per_request_m = int(round(self.clock_accuracy / max_rate))
|
||||
running_time_m = self.memcache_client.incr(key,
|
||||
delta=time_per_request_m)
|
||||
need_to_sleep_m = 0
|
||||
if (now_m - running_time_m >
|
||||
self.rate_buffer_seconds * self.clock_accuracy):
|
||||
next_avail_time = int(now_m + time_per_request_m)
|
||||
self.memcache_client.set(key, str(next_avail_time),
|
||||
serialize=False)
|
||||
else:
|
||||
need_to_sleep_m = \
|
||||
max(running_time_m - now_m - time_per_request_m, 0)
|
||||
|
||||
max_sleep_m = self.max_sleep_time_seconds * self.clock_accuracy
|
||||
if max_sleep_m - need_to_sleep_m <= self.clock_accuracy * 0.01:
|
||||
# treat as no-op decrement time
|
||||
self.memcache_client.decr(key, delta=time_per_request_m)
|
||||
raise MaxSleepTimeHitError("Max Sleep Time Exceeded: %.2f" %
|
||||
(float(need_to_sleep_m) / self.clock_accuracy))
|
||||
|
||||
return float(need_to_sleep_m) / self.clock_accuracy
|
||||
except MemcacheConnectionError:
|
||||
return 0
|
||||
|
||||
def handle_ratelimit(self, req, account_name, container_name, obj_name):
|
||||
'''
|
||||
Performs rate limiting and account white/black listing. Sleeps
|
||||
if necessary.
|
||||
|
||||
:param account_name: account name from path
|
||||
:param container_name: container name from path
|
||||
:param obj_name: object name from path
|
||||
'''
|
||||
if account_name in self.ratelimit_blacklist:
|
||||
self.logger.error(_('Returning 497 because of blacklisting: %s'),
|
||||
account_name)
|
||||
eventlet.sleep(self.BLACK_LIST_SLEEP)
|
||||
return Response(status='497 Blacklisted',
|
||||
body='Your account has been blacklisted', request=req)
|
||||
if account_name in self.ratelimit_whitelist:
|
||||
return None
|
||||
for key, max_rate in self.get_ratelimitable_key_tuples(
|
||||
req.method, account_name, container_name=container_name,
|
||||
obj_name=obj_name):
|
||||
try:
|
||||
need_to_sleep = self._get_sleep_time(key, max_rate)
|
||||
if self.log_sleep_time_seconds and \
|
||||
need_to_sleep > self.log_sleep_time_seconds:
|
||||
self.logger.warning(_("Ratelimit sleep log: %(sleep)s for "
|
||||
"%(account)s/%(container)s/%(object)s"),
|
||||
{'sleep': need_to_sleep, 'account': account_name,
|
||||
'container': container_name, 'object': obj_name})
|
||||
if need_to_sleep > 0:
|
||||
eventlet.sleep(need_to_sleep)
|
||||
except MaxSleepTimeHitError, e:
|
||||
self.logger.error(_('Returning 498 for %(meth)s to '
|
||||
'%(acc)s/%(cont)s/%(obj)s . Ratelimit (Max Sleep) %(e)s'),
|
||||
{'meth': req.method, 'acc': account_name,
|
||||
'cont': container_name, 'obj': obj_name, 'e': str(e)})
|
||||
error_resp = Response(status='498 Rate Limited',
|
||||
body='Slow down', request=req)
|
||||
return error_resp
|
||||
return None
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
"""
|
||||
WSGI entry point.
|
||||
Wraps env in webob.Request object and passes it down.
|
||||
|
||||
:param env: WSGI environment dictionary
|
||||
:param start_response: WSGI callable
|
||||
"""
|
||||
req = Request(env)
|
||||
if self.memcache_client is None:
|
||||
self.memcache_client = cache_from_env(env)
|
||||
if not self.memcache_client:
|
||||
self.logger.warning(
|
||||
_('Warning: Cannot ratelimit without a memcached client'))
|
||||
return self.app(env, start_response)
|
||||
try:
|
||||
version, account, container, obj = split_path(req.path, 1, 4, True)
|
||||
except ValueError:
|
||||
return self.app(env, start_response)
|
||||
ratelimit_resp = self.handle_ratelimit(req, account, container, obj)
|
||||
if ratelimit_resp is None:
|
||||
return self.app(env, start_response)
|
||||
else:
|
||||
return ratelimit_resp(env, start_response)
|
||||
|
||||
|
||||
def filter_factory(global_conf, **local_conf):
|
||||
"""
|
||||
paste.deploy app factory for creating WSGI proxy apps.
|
||||
"""
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
||||
def limit_filter(app):
|
||||
return RateLimitMiddleware(app, conf)
|
||||
return limit_filter
|
@ -1,425 +0,0 @@
|
||||
# Copyright (c) 2010-2012 OpenStack, LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import unittest
|
||||
import time
|
||||
import eventlet
|
||||
from contextlib import contextmanager
|
||||
from threading import Thread
|
||||
from webob import Request
|
||||
|
||||
from test.unit import FakeLogger
|
||||
from swift.common.middleware import ratelimit
|
||||
from swift.proxy.server import get_container_memcache_key
|
||||
from swift.common.memcached import MemcacheConnectionError
|
||||
|
||||
|
||||
class FakeMemcache(object):
|
||||
|
||||
def __init__(self):
|
||||
self.store = {}
|
||||
self.error_on_incr = False
|
||||
self.init_incr_return_neg = False
|
||||
|
||||
def get(self, key):
|
||||
return self.store.get(key)
|
||||
|
||||
def set(self, key, value, serialize=False, timeout=0):
|
||||
self.store[key] = value
|
||||
return True
|
||||
|
||||
def incr(self, key, delta=1, timeout=0):
|
||||
if self.error_on_incr:
|
||||
raise MemcacheConnectionError('Memcache restarting')
|
||||
if self.init_incr_return_neg:
|
||||
# simulate initial hit, force reset of memcache
|
||||
self.init_incr_return_neg = False
|
||||
return -10000000
|
||||
self.store[key] = int(self.store.setdefault(key, 0)) + int(delta)
|
||||
if self.store[key] < 0:
|
||||
self.store[key] = 0
|
||||
return int(self.store[key])
|
||||
|
||||
def decr(self, key, delta=1, timeout=0):
|
||||
return self.incr(key, delta=-delta, timeout=timeout)
|
||||
|
||||
@contextmanager
|
||||
def soft_lock(self, key, timeout=0, retries=5):
|
||||
yield True
|
||||
|
||||
def delete(self, key):
|
||||
try:
|
||||
del self.store[key]
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def mock_http_connect(response, headers=None, with_exc=False):
|
||||
|
||||
class FakeConn(object):
|
||||
|
||||
def __init__(self, status, headers, with_exc):
|
||||
self.status = status
|
||||
self.reason = 'Fake'
|
||||
self.host = '1.2.3.4'
|
||||
self.port = '1234'
|
||||
self.with_exc = with_exc
|
||||
self.headers = headers
|
||||
if self.headers is None:
|
||||
self.headers = {}
|
||||
|
||||
def getresponse(self):
|
||||
if self.with_exc:
|
||||
raise Exception('test')
|
||||
return self
|
||||
|
||||
def getheader(self, header):
|
||||
return self.headers[header]
|
||||
|
||||
def read(self, amt=None):
|
||||
return ''
|
||||
|
||||
def close(self):
|
||||
return
|
||||
return lambda *args, **kwargs: FakeConn(response, headers, with_exc)
|
||||
|
||||
|
||||
class FakeApp(object):
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
return ['204 No Content']
|
||||
|
||||
|
||||
def start_response(*args):
|
||||
pass
|
||||
|
||||
|
||||
def dummy_filter_factory(global_conf, **local_conf):
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
|
||||
def limit_filter(app):
|
||||
return ratelimit.RateLimitMiddleware(app, conf, logger=FakeLogger())
|
||||
return limit_filter
|
||||
|
||||
time_ticker = 0
|
||||
time_override = []
|
||||
|
||||
|
||||
def mock_sleep(x):
|
||||
global time_ticker
|
||||
time_ticker += x
|
||||
|
||||
|
||||
def mock_time():
|
||||
global time_override
|
||||
global time_ticker
|
||||
if time_override:
|
||||
cur_time = time_override.pop(0)
|
||||
if cur_time is None:
|
||||
time_override = [None if i is None else i + time_ticker
|
||||
for i in time_override]
|
||||
return time_ticker
|
||||
return cur_time
|
||||
return time_ticker
|
||||
|
||||
|
||||
class TestRateLimit(unittest.TestCase):
|
||||
|
||||
def _reset_time(self):
|
||||
global time_ticker
|
||||
time_ticker = 0
|
||||
|
||||
def setUp(self):
|
||||
self.was_sleep = eventlet.sleep
|
||||
eventlet.sleep = mock_sleep
|
||||
self.was_time = time.time
|
||||
time.time = mock_time
|
||||
self._reset_time()
|
||||
|
||||
def tearDown(self):
|
||||
eventlet.sleep = self.was_sleep
|
||||
time.time = self.was_time
|
||||
|
||||
def _run(self, callable_func, num, rate, check_time=True):
|
||||
global time_ticker
|
||||
begin = time.time()
|
||||
for x in range(0, num):
|
||||
result = callable_func()
|
||||
end = time.time()
|
||||
total_time = float(num) / rate - 1.0 / rate # 1st request isn't limited
|
||||
# Allow for one second of variation in the total time.
|
||||
time_diff = abs(total_time - (end - begin))
|
||||
if check_time:
|
||||
self.assertEquals(round(total_time, 1), round(time_ticker, 1))
|
||||
return time_diff
|
||||
|
||||
def test_get_container_maxrate(self):
|
||||
conf_dict = {'container_ratelimit_10': 200,
|
||||
'container_ratelimit_50': 100,
|
||||
'container_ratelimit_75': 30}
|
||||
test_ratelimit = dummy_filter_factory(conf_dict)(FakeApp())
|
||||
self.assertEquals(test_ratelimit.get_container_maxrate(0), None)
|
||||
self.assertEquals(test_ratelimit.get_container_maxrate(5), None)
|
||||
self.assertEquals(test_ratelimit.get_container_maxrate(10), 200)
|
||||
self.assertEquals(test_ratelimit.get_container_maxrate(60), 72)
|
||||
self.assertEquals(test_ratelimit.get_container_maxrate(160), 30)
|
||||
|
||||
def test_get_ratelimitable_key_tuples(self):
|
||||
current_rate = 13
|
||||
conf_dict = {'account_ratelimit': current_rate,
|
||||
'container_ratelimit_3': 200}
|
||||
fake_memcache = FakeMemcache()
|
||||
fake_memcache.store[get_container_memcache_key('a', 'c')] = \
|
||||
{'container_size': 5}
|
||||
the_app = ratelimit.RateLimitMiddleware(None, conf_dict,
|
||||
logger=FakeLogger())
|
||||
the_app.memcache_client = fake_memcache
|
||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
||||
'DELETE', 'a', None, None)), 0)
|
||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
||||
'PUT', 'a', 'c', None)), 1)
|
||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
||||
'DELETE', 'a', 'c', None)), 1)
|
||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
||||
'GET', 'a', 'c', 'o')), 0)
|
||||
self.assertEquals(len(the_app.get_ratelimitable_key_tuples(
|
||||
'PUT', 'a', 'c', 'o')), 1)
|
||||
|
||||
def test_account_ratelimit(self):
|
||||
current_rate = 5
|
||||
num_calls = 50
|
||||
conf_dict = {'account_ratelimit': current_rate}
|
||||
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
|
||||
ratelimit.http_connect = mock_http_connect(204)
|
||||
for meth, exp_time in [('DELETE', 9.8), ('GET', 0),
|
||||
('POST', 0), ('PUT', 9.8)]:
|
||||
req = Request.blank('/v/a%s/c' % meth)
|
||||
req.method = meth
|
||||
req.environ['swift.cache'] = FakeMemcache()
|
||||
make_app_call = lambda: self.test_ratelimit(req.environ,
|
||||
start_response)
|
||||
begin = time.time()
|
||||
self._run(make_app_call, num_calls, current_rate,
|
||||
check_time=bool(exp_time))
|
||||
self.assertEquals(round(time.time() - begin, 1), exp_time)
|
||||
self._reset_time()
|
||||
|
||||
def test_ratelimit_set_incr(self):
|
||||
current_rate = 5
|
||||
num_calls = 50
|
||||
conf_dict = {'account_ratelimit': current_rate}
|
||||
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
|
||||
ratelimit.http_connect = mock_http_connect(204)
|
||||
req = Request.blank('/v/a/c')
|
||||
req.method = 'PUT'
|
||||
req.environ['swift.cache'] = FakeMemcache()
|
||||
req.environ['swift.cache'].init_incr_return_neg = True
|
||||
make_app_call = lambda: self.test_ratelimit(req.environ,
|
||||
start_response)
|
||||
begin = time.time()
|
||||
self._run(make_app_call, num_calls, current_rate, check_time=False)
|
||||
self.assertEquals(round(time.time() - begin, 1), 9.8)
|
||||
|
||||
def test_ratelimit_whitelist(self):
|
||||
global time_ticker
|
||||
current_rate = 2
|
||||
conf_dict = {'account_ratelimit': current_rate,
|
||||
'max_sleep_time_seconds': 2,
|
||||
'account_whitelist': 'a',
|
||||
'account_blacklist': 'b'}
|
||||
self.test_ratelimit = dummy_filter_factory(conf_dict)(FakeApp())
|
||||
ratelimit.http_connect = mock_http_connect(204)
|
||||
req = Request.blank('/v/a/c')
|
||||
req.environ['swift.cache'] = FakeMemcache()
|
||||
|
||||
class rate_caller(Thread):
|
||||
|
||||
def __init__(self, parent):
|
||||
Thread.__init__(self)
|
||||
self.parent = parent
|
||||
|
||||
def run(self):
|
||||
self.result = self.parent.test_ratelimit(req.environ,
|
||||
start_response)
|
||||
nt = 5
|
||||
threads = []
|
||||
for i in range(nt):
|
||||
rc = rate_caller(self)
|
||||
rc.start()
|
||||
threads.append(rc)
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
the_498s = [t for t in threads if \
|
||||
''.join(t.result).startswith('Slow down')]
|
||||
self.assertEquals(len(the_498s), 0)
|
||||
self.assertEquals(time_ticker, 0)
|
||||
|
||||
def test_ratelimit_blacklist(self):
|
||||
global time_ticker
|
||||
current_rate = 2
|
||||
conf_dict = {'account_ratelimit': current_rate,
|
||||
'max_sleep_time_seconds': 2,
|
||||
'account_whitelist': 'a',
|
||||
'account_blacklist': 'b'}
|
||||
self.test_ratelimit = dummy_filter_factory(conf_dict)(FakeApp())
|
||||
self.test_ratelimit.BLACK_LIST_SLEEP = 0
|
||||
ratelimit.http_connect = mock_http_connect(204)
|
||||
req = Request.blank('/v/b/c')
|
||||
req.environ['swift.cache'] = FakeMemcache()
|
||||
|
||||
class rate_caller(Thread):
|
||||
|
||||
def __init__(self, parent):
|
||||
Thread.__init__(self)
|
||||
self.parent = parent
|
||||
|
||||
def run(self):
|
||||
self.result = self.parent.test_ratelimit(req.environ,
|
||||
start_response)
|
||||
nt = 5
|
||||
threads = []
|
||||
for i in range(nt):
|
||||
rc = rate_caller(self)
|
||||
rc.start()
|
||||
threads.append(rc)
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
the_497s = [t for t in threads if \
|
||||
''.join(t.result).startswith('Your account')]
|
||||
self.assertEquals(len(the_497s), 5)
|
||||
self.assertEquals(time_ticker, 0)
|
||||
|
||||
def test_ratelimit_max_rate_double(self):
|
||||
global time_ticker
|
||||
global time_override
|
||||
current_rate = 2
|
||||
conf_dict = {'account_ratelimit': current_rate,
|
||||
'clock_accuracy': 100,
|
||||
'max_sleep_time_seconds': 1}
|
||||
self.test_ratelimit = dummy_filter_factory(conf_dict)(FakeApp())
|
||||
ratelimit.http_connect = mock_http_connect(204)
|
||||
self.test_ratelimit.log_sleep_time_seconds = .00001
|
||||
req = Request.blank('/v/a/c')
|
||||
req.method = 'PUT'
|
||||
req.environ['swift.cache'] = FakeMemcache()
|
||||
|
||||
time_override = [0, 0, 0, 0, None]
|
||||
# simulates 4 requests coming in at same time, then sleeping
|
||||
r = self.test_ratelimit(req.environ, start_response)
|
||||
mock_sleep(.1)
|
||||
r = self.test_ratelimit(req.environ, start_response)
|
||||
mock_sleep(.1)
|
||||
r = self.test_ratelimit(req.environ, start_response)
|
||||
self.assertEquals(r[0], 'Slow down')
|
||||
mock_sleep(.1)
|
||||
r = self.test_ratelimit(req.environ, start_response)
|
||||
self.assertEquals(r[0], 'Slow down')
|
||||
mock_sleep(.1)
|
||||
r = self.test_ratelimit(req.environ, start_response)
|
||||
self.assertEquals(r[0], '204 No Content')
|
||||
|
||||
def test_ratelimit_max_rate_multiple_acc(self):
|
||||
num_calls = 4
|
||||
current_rate = 2
|
||||
conf_dict = {'account_ratelimit': current_rate,
|
||||
'max_sleep_time_seconds': 2}
|
||||
fake_memcache = FakeMemcache()
|
||||
|
||||
the_app = ratelimit.RateLimitMiddleware(None, conf_dict,
|
||||
logger=FakeLogger())
|
||||
the_app.memcache_client = fake_memcache
|
||||
req = lambda: None
|
||||
req.method = 'PUT'
|
||||
|
||||
class rate_caller(Thread):
|
||||
|
||||
def __init__(self, name):
|
||||
self.myname = name
|
||||
Thread.__init__(self)
|
||||
|
||||
def run(self):
|
||||
for j in range(num_calls):
|
||||
self.result = the_app.handle_ratelimit(req, self.myname,
|
||||
'c', None)
|
||||
|
||||
nt = 15
|
||||
begin = time.time()
|
||||
threads = []
|
||||
for i in range(nt):
|
||||
rc = rate_caller('a%s' % i)
|
||||
rc.start()
|
||||
threads.append(rc)
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
time_took = time.time() - begin
|
||||
self.assertEquals(1.5, round(time_took, 1))
|
||||
|
||||
def test_call_invalid_path(self):
|
||||
env = {'REQUEST_METHOD': 'GET',
|
||||
'SCRIPT_NAME': '',
|
||||
'PATH_INFO': '//v1/AUTH_1234567890',
|
||||
'SERVER_NAME': '127.0.0.1',
|
||||
'SERVER_PORT': '80',
|
||||
'swift.cache': FakeMemcache(),
|
||||
'SERVER_PROTOCOL': 'HTTP/1.0'}
|
||||
|
||||
app = lambda *args, **kwargs: ['fake_app']
|
||||
rate_mid = ratelimit.RateLimitMiddleware(app, {},
|
||||
logger=FakeLogger())
|
||||
|
||||
class a_callable(object):
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
pass
|
||||
resp = rate_mid.__call__(env, a_callable())
|
||||
self.assert_('fake_app' == resp[0])
|
||||
|
||||
def test_no_memcache(self):
|
||||
current_rate = 13
|
||||
num_calls = 5
|
||||
conf_dict = {'account_ratelimit': current_rate}
|
||||
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
|
||||
ratelimit.http_connect = mock_http_connect(204)
|
||||
req = Request.blank('/v/a')
|
||||
req.environ['swift.cache'] = None
|
||||
make_app_call = lambda: self.test_ratelimit(req.environ,
|
||||
start_response)
|
||||
begin = time.time()
|
||||
self._run(make_app_call, num_calls, current_rate, check_time=False)
|
||||
time_took = time.time() - begin
|
||||
self.assertEquals(round(time_took, 1), 0) # no memcache, no limiting
|
||||
|
||||
def test_restarting_memcache(self):
|
||||
current_rate = 2
|
||||
num_calls = 5
|
||||
conf_dict = {'account_ratelimit': current_rate}
|
||||
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
|
||||
ratelimit.http_connect = mock_http_connect(204)
|
||||
req = Request.blank('/v/a/c')
|
||||
req.method = 'PUT'
|
||||
req.environ['swift.cache'] = FakeMemcache()
|
||||
req.environ['swift.cache'].error_on_incr = True
|
||||
make_app_call = lambda: self.test_ratelimit(req.environ,
|
||||
start_response)
|
||||
begin = time.time()
|
||||
self._run(make_app_call, num_calls, current_rate, check_time=False)
|
||||
time_took = time.time() - begin
|
||||
self.assertEquals(round(time_took, 1), 0) # no memcache, no limiting
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user