Skip to content

Commit 1db11df

Browse files
committed
ratelimit: Allow multiple placements
We usually want to have ratelimit fairly far left in the pipeline -- the assumption is that something like an auth check will be fairly expensive and we should try to shield the auth system so it doesn't melt under the load of a misbehaved swift client. But with S3 requests, we can't know the account/container that a request is destined for until *after* auth. Fortunately, we've already got some code to make s3api play well with ratelimit. So, let's have our cake and eat it, too: allow operators to place ratelimit once, before auth, for swift requests and again, after auth, for s3api. They'll both use the same memcached keys (so users can't switch APIs to effectively double their limit), but still only have each S3 request counted against the limit once. Change-Id: If003bb43f39427fe47a0f5a01dbcc19e1b3b67ef
1 parent 70dede1 commit 1db11df

File tree

3 files changed

+73
-50
lines changed

3 files changed

+73
-50
lines changed

etc/proxy-server.conf-sample

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,10 @@ use = egg:swift#s3api
470470
# With either tempauth or your custom auth:
471471
# - Put s3api just before your auth filter(s) in the pipeline
472472
# With keystone:
473-
# - Put s3api and s3token before keystoneauth in the pipeline
473+
# - Put s3api and s3token before keystoneauth in the pipeline, but after
474+
# auth_token
475+
# If you have ratelimit enabled for Swift requests, you may want to place a
476+
# second copy after auth to also ratelimit S3 requests.
474477
#
475478
# Swift has no concept of the S3's resource owner; the resources
476479
# (i.e. containers and objects) created via the Swift API have no owner

swift/common/middleware/ratelimit.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,10 @@ def handle_ratelimit(self, req, account_name, container_name, obj_name):
242242
if not self.memcache_client:
243243
return None
244244

245+
if req.environ.get('swift.ratelimit.handled'):
246+
return None
247+
req.environ['swift.ratelimit.handled'] = True
248+
245249
try:
246250
account_info = get_account_info(req.environ, self.app,
247251
swift_source='RL')

test/unit/common/middleware/test_ratelimit.py

Lines changed: 65 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,20 @@ def delete(self, key):
7272

7373

7474
class FakeApp(object):
75+
skip_handled_check = False
7576

7677
def __call__(self, env, start_response):
78+
assert self.skip_handled_check or env.get('swift.ratelimit.handled')
7779
start_response('200 OK', [])
7880
return [b'Some Content']
7981

8082

83+
class FakeReq(object):
84+
def __init__(self, method, env=None):
85+
self.method = method
86+
self.environ = env or {}
87+
88+
8189
def start_response(*args):
8290
pass
8391

@@ -160,36 +168,29 @@ def test_get_ratelimitable_key_tuples(self):
160168
{'object_count': '5'}
161169
the_app = ratelimit.filter_factory(conf_dict)(FakeApp())
162170
the_app.memcache_client = fake_memcache
163-
req = lambda: None
164-
req.environ = {'swift.cache': fake_memcache, 'PATH_INFO': '/v1/a/c/o'}
171+
environ = {'swift.cache': fake_memcache, 'PATH_INFO': '/v1/a/c/o'}
165172
with mock.patch('swift.common.middleware.ratelimit.get_account_info',
166173
lambda *args, **kwargs: {}):
167-
req.method = 'DELETE'
168174
self.assertEqual(len(the_app.get_ratelimitable_key_tuples(
169-
req, 'a', None, None)), 0)
170-
req.method = 'PUT'
175+
FakeReq('DELETE', environ), 'a', None, None)), 0)
171176
self.assertEqual(len(the_app.get_ratelimitable_key_tuples(
172-
req, 'a', 'c', None)), 1)
173-
req.method = 'DELETE'
177+
FakeReq('PUT', environ), 'a', 'c', None)), 1)
174178
self.assertEqual(len(the_app.get_ratelimitable_key_tuples(
175-
req, 'a', 'c', None)), 1)
176-
req.method = 'GET'
179+
FakeReq('DELETE', environ), 'a', 'c', None)), 1)
177180
self.assertEqual(len(the_app.get_ratelimitable_key_tuples(
178-
req, 'a', 'c', 'o')), 0)
179-
req.method = 'PUT'
181+
FakeReq('GET', environ), 'a', 'c', 'o')), 0)
180182
self.assertEqual(len(the_app.get_ratelimitable_key_tuples(
181-
req, 'a', 'c', 'o')), 1)
183+
FakeReq('PUT', environ), 'a', 'c', 'o')), 1)
182184

183-
req.method = 'PUT'
184185
self.assertEqual(len(the_app.get_ratelimitable_key_tuples(
185-
req, 'a', 'c', None, global_ratelimit=10)), 2)
186+
FakeReq('PUT', environ), 'a', 'c', None, global_ratelimit=10)), 2)
186187
self.assertEqual(the_app.get_ratelimitable_key_tuples(
187-
req, 'a', 'c', None, global_ratelimit=10)[1],
188+
FakeReq('PUT', environ), 'a', 'c', None, global_ratelimit=10)[1],
188189
('ratelimit/global-write/a', 10))
189190

190-
req.method = 'PUT'
191191
self.assertEqual(len(the_app.get_ratelimitable_key_tuples(
192-
req, 'a', 'c', None, global_ratelimit='notafloat')), 1)
192+
FakeReq('PUT', environ), 'a', 'c', None,
193+
global_ratelimit='notafloat')), 1)
193194

194195
def test_memcached_container_info_dict(self):
195196
mdict = headers_to_container_info({'x-container-object-count': '45'})
@@ -204,9 +205,8 @@ def test_ratelimit_old_memcache_format(self):
204205
{'container_size': 5}
205206
the_app = ratelimit.filter_factory(conf_dict)(FakeApp())
206207
the_app.memcache_client = fake_memcache
207-
req = lambda: None
208-
req.method = 'PUT'
209-
req.environ = {'PATH_INFO': '/v1/a/c/o', 'swift.cache': fake_memcache}
208+
req = FakeReq('PUT', {
209+
'PATH_INFO': '/v1/a/c/o', 'swift.cache': fake_memcache})
210210
with mock.patch('swift.common.middleware.ratelimit.get_account_info',
211211
lambda *args, **kwargs: {}):
212212
tuples = the_app.get_ratelimitable_key_tuples(req, 'a', 'c', 'o')
@@ -227,8 +227,8 @@ def test_account_ratelimit(self):
227227
req = Request.blank('/v1/a%s/c' % meth)
228228
req.method = meth
229229
req.environ['swift.cache'] = FakeMemcache()
230-
make_app_call = lambda: self.test_ratelimit(req.environ,
231-
start_response)
230+
make_app_call = lambda: self.test_ratelimit(
231+
req.environ.copy(), start_response)
232232
begin = time.time()
233233
self._run(make_app_call, num_calls, current_rate,
234234
check_time=bool(exp_time))
@@ -244,7 +244,7 @@ def test_ratelimit_set_incr(self):
244244
req.method = 'PUT'
245245
req.environ['swift.cache'] = FakeMemcache()
246246
req.environ['swift.cache'].init_incr_return_neg = True
247-
make_app_call = lambda: self.test_ratelimit(req.environ,
247+
make_app_call = lambda: self.test_ratelimit(req.environ.copy(),
248248
start_response)
249249
begin = time.time()
250250
with mock.patch('swift.common.middleware.ratelimit.get_account_info',
@@ -260,15 +260,15 @@ def test_ratelimit_old_white_black_list(self):
260260
'account_whitelist': 'a',
261261
'account_blacklist': 'b'}
262262
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
263-
req = Request.blank('/')
264263
with mock.patch.object(self.test_ratelimit,
265264
'memcache_client', FakeMemcache()):
266265
self.assertEqual(
267-
self.test_ratelimit.handle_ratelimit(req, 'a', 'c', 'o'),
266+
self.test_ratelimit.handle_ratelimit(
267+
Request.blank('/'), 'a', 'c', 'o'),
268268
None)
269269
self.assertEqual(
270270
self.test_ratelimit.handle_ratelimit(
271-
req, 'b', 'c', 'o').status_int,
271+
Request.blank('/'), 'b', 'c', 'o').status_int,
272272
497)
273273

274274
def test_ratelimit_whitelist_sysmeta(self):
@@ -331,7 +331,7 @@ def __init__(self, parent):
331331
self.parent = parent
332332

333333
def run(self):
334-
self.result = self.parent.test_ratelimit(req.environ,
334+
self.result = self.parent.test_ratelimit(req.environ.copy(),
335335
start_response)
336336

337337
def get_fake_ratelimit(*args, **kwargs):
@@ -370,18 +370,17 @@ def test_ratelimit_max_rate_double(self):
370370
# simulates 4 requests coming in at same time, then sleeping
371371
with mock.patch('swift.common.middleware.ratelimit.get_account_info',
372372
lambda *args, **kwargs: {}):
373-
r = self.test_ratelimit(req.environ, start_response)
373+
r = self.test_ratelimit(req.environ.copy(), start_response)
374374
mock_sleep(.1)
375-
r = self.test_ratelimit(req.environ, start_response)
375+
r = self.test_ratelimit(req.environ.copy(), start_response)
376376
mock_sleep(.1)
377-
r = self.test_ratelimit(req.environ, start_response)
377+
r = self.test_ratelimit(req.environ.copy(), start_response)
378378
self.assertEqual(r[0], b'Slow down')
379379
mock_sleep(.1)
380-
r = self.test_ratelimit(req.environ, start_response)
380+
r = self.test_ratelimit(req.environ.copy(), start_response)
381381
self.assertEqual(r[0], b'Slow down')
382382
mock_sleep(.1)
383-
r = self.test_ratelimit(req.environ, start_response)
384-
print(repr(r))
383+
r = self.test_ratelimit(req.environ.copy(), start_response)
385384
self.assertEqual(r[0], b'Some Content')
386385

387386
def test_ratelimit_max_rate_double_container(self):
@@ -404,17 +403,17 @@ def test_ratelimit_max_rate_double_container(self):
404403
# simulates 4 requests coming in at same time, then sleeping
405404
with mock.patch('swift.common.middleware.ratelimit.get_account_info',
406405
lambda *args, **kwargs: {}):
407-
r = self.test_ratelimit(req.environ, start_response)
406+
r = self.test_ratelimit(req.environ.copy(), start_response)
408407
mock_sleep(.1)
409-
r = self.test_ratelimit(req.environ, start_response)
408+
r = self.test_ratelimit(req.environ.copy(), start_response)
410409
mock_sleep(.1)
411-
r = self.test_ratelimit(req.environ, start_response)
410+
r = self.test_ratelimit(req.environ.copy(), start_response)
412411
self.assertEqual(r[0], b'Slow down')
413412
mock_sleep(.1)
414-
r = self.test_ratelimit(req.environ, start_response)
413+
r = self.test_ratelimit(req.environ.copy(), start_response)
415414
self.assertEqual(r[0], b'Slow down')
416415
mock_sleep(.1)
417-
r = self.test_ratelimit(req.environ, start_response)
416+
r = self.test_ratelimit(req.environ.copy(), start_response)
418417
self.assertEqual(r[0], b'Some Content')
419418

420419
def test_ratelimit_max_rate_double_container_listing(self):
@@ -437,17 +436,17 @@ def test_ratelimit_max_rate_double_container_listing(self):
437436
lambda *args, **kwargs: {}):
438437
time_override = [0, 0, 0, 0, None]
439438
# simulates 4 requests coming in at same time, then sleeping
440-
r = self.test_ratelimit(req.environ, start_response)
439+
r = self.test_ratelimit(req.environ.copy(), start_response)
441440
mock_sleep(.1)
442-
r = self.test_ratelimit(req.environ, start_response)
441+
r = self.test_ratelimit(req.environ.copy(), start_response)
443442
mock_sleep(.1)
444-
r = self.test_ratelimit(req.environ, start_response)
443+
r = self.test_ratelimit(req.environ.copy(), start_response)
445444
self.assertEqual(r[0], b'Slow down')
446445
mock_sleep(.1)
447-
r = self.test_ratelimit(req.environ, start_response)
446+
r = self.test_ratelimit(req.environ.copy(), start_response)
448447
self.assertEqual(r[0], b'Slow down')
449448
mock_sleep(.1)
450-
r = self.test_ratelimit(req.environ, start_response)
449+
r = self.test_ratelimit(req.environ.copy(), start_response)
451450
self.assertEqual(r[0], b'Some Content')
452451
mc = self.test_ratelimit.memcache_client
453452
try:
@@ -466,9 +465,6 @@ def test_ratelimit_max_rate_multiple_acc(self):
466465

467466
the_app = ratelimit.filter_factory(conf_dict)(FakeApp())
468467
the_app.memcache_client = fake_memcache
469-
req = lambda: None
470-
req.method = 'PUT'
471-
req.environ = {}
472468

473469
class rate_caller(threading.Thread):
474470

@@ -478,8 +474,8 @@ def __init__(self, name):
478474

479475
def run(self):
480476
for j in range(num_calls):
481-
self.result = the_app.handle_ratelimit(req, self.myname,
482-
'c', None)
477+
self.result = the_app.handle_ratelimit(
478+
FakeReq('PUT'), self.myname, 'c', None)
483479

484480
with mock.patch('swift.common.middleware.ratelimit.get_account_info',
485481
lambda *args, **kwargs: {}):
@@ -541,7 +537,9 @@ def test_no_memcache(self):
541537
current_rate = 13
542538
num_calls = 5
543539
conf_dict = {'account_ratelimit': current_rate}
544-
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
540+
fake_app = FakeApp()
541+
fake_app.skip_handled_check = True
542+
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(fake_app)
545543
req = Request.blank('/v1/a')
546544
req.environ['swift.cache'] = None
547545
make_app_call = lambda: self.test_ratelimit(req.environ,
@@ -551,6 +549,24 @@ def test_no_memcache(self):
551549
time_took = time.time() - begin
552550
self.assertEqual(round(time_took, 1), 0) # no memcache, no limiting
553551

552+
def test_already_handled(self):
553+
current_rate = 13
554+
num_calls = 5
555+
conf_dict = {'container_listing_ratelimit_0': current_rate}
556+
self.test_ratelimit = ratelimit.filter_factory(conf_dict)(FakeApp())
557+
fake_cache = FakeMemcache()
558+
fake_cache.set(
559+
get_cache_key('a', 'c'),
560+
{'object_count': 1})
561+
req = Request.blank('/v1/a/c', environ={'swift.cache': fake_cache})
562+
req.environ['swift.ratelimit.handled'] = True
563+
make_app_call = lambda: self.test_ratelimit(req.environ,
564+
start_response)
565+
begin = time.time()
566+
self._run(make_app_call, num_calls, current_rate, check_time=False)
567+
time_took = time.time() - begin
568+
self.assertEqual(round(time_took, 1), 0) # no memcache, no limiting
569+
554570
def test_restarting_memcache(self):
555571
current_rate = 2
556572
num_calls = 5

0 commit comments

Comments
 (0)