jduff-api-throttling 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +22 -0
- data/VERSION.yml +1 -1
- data/lib/handlers/active_support_cache_store_handler.rb +21 -0
- data/lib/handlers/handlers.rb +17 -17
- data/lib/handlers/hash_handler.rb +2 -0
- data/lib/handlers/memcache_handler.rb +2 -2
- data/lib/handlers/redis_handler.rb +2 -1
- data/test/test_api_throttling.rb +45 -1
- data/test/test_api_throttling_memcache.rb +1 -1
- data/test/test_handlers.rb +15 -29
- metadata +2 -1
data/README.md
CHANGED
@@ -2,6 +2,28 @@
|
|
2
2
|
|
3
3
|
<p>I will show you a technique to impose a rate limit (aka API Throttling) on a Ruby Web Service. I will be using Rack middleware so you can use this no matter what Ruby Web Framework you are using, as long as it is Rack-compliant.</p>
|
4
4
|
|
5
|
+
<h2>Installation</h2>
|
6
|
+
<p>This middleware has recently been gemmified, you can install the latest gem using:</p>
|
7
|
+
<pre>sudo gem install jduff-api-throttling</pre>
|
8
|
+
<p>If you prefer to have the latest source it can be found at http://github.com/jduff/api-throttling/tree (this is a fork of http://github.com/dambalah/api-throttling/tree with a number of recent changes)</p>
|
9
|
+
|
10
|
+
<h2>Usage</h2>
|
11
|
+
<p>In your rack application simply use the middleware and pass it some options</p>
|
12
|
+
<pre>use ApiThrottling, :requests_per_hour => 3</pre>
|
13
|
+
<p>This will setup throttling with a limit of 3 requests per hour and will use a Redis cache to keep track of it. By default Rack::Auth::Basic is used to limit the requests on a per user basis.</p>
|
14
|
+
<p>A number of options can be passed to the middleware so it can be configured as needed for your stack.</p>
|
15
|
+
<pre>:cache=>:redis # :memcache, :hash are supported. you can also pass in an instance of those caches, or even Rails.cache</pre>
|
16
|
+
<pre>:auth=>false # if your middleware is doing authentication somewhere else</pre>
|
17
|
+
<pre>:key=>Proc.new{|env,auth| "#{env['PATH_INFO']}_#{Time.now.strftime("%Y-%m-%d-%H")}" } # to customize how the cache key is generated</pre>
|
18
|
+
|
19
|
+
<p>An example using all the options might look something like this:</p>
|
20
|
+
<pre>
|
21
|
+
CACHE = MemCache.new
|
22
|
+
use ApiThrottling, :requests_per_hour => 100, :cache=>CACHE, :auth=>false,
|
23
|
+
:key=>Proc.new{|env,auth| "#{env['PATH_INFO']}_#{Time.now.strftime("%Y-%m-%d-%H")}" }
|
24
|
+
</pre>
|
25
|
+
<p>This will limit requests to 100 per hour per url ('/home' will be tracked separately from '/users') keeping track by storing the counts with MemCache.</p>
|
26
|
+
|
5
27
|
<h2>Introduction to Rack</h2>
|
6
28
|
|
7
29
|
<p>There are plenty of <a href="http://jasonseifer.com/2009/04/08/32-rack-resources-to-get-you-started">great resources</a> to learn the basic of Rack so I will not be explaining how Rack works here but you will need to understand it in order to follow this post. I highly recommend watching the <a href="http://remi.org/2009/02/19/rack-basics.html">three</a> <a href="http://remi.org/2009/02/24/rack-part-2.html">Rack</a> <a href="http://remi.org/2009/02/28/rack-part-3-middleware.html">screencasts</a> from <a href="http://remi.org/">Remi</a> to get started with Rack.</p>
|
data/VERSION.yml
CHANGED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Handlers
|
2
|
+
class ActiveSupportCacheStoreHandler < Handler
|
3
|
+
|
4
|
+
def initialize(object=nil)
|
5
|
+
raise "Must provide an existing ActiveSupport::Cache::Store" unless object.is_a?(ActiveSupport::Cache::Store)
|
6
|
+
@cache = object
|
7
|
+
end
|
8
|
+
|
9
|
+
def increment(key)
|
10
|
+
@cache.write(key, (get(key)||0).to_i+1)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(key)
|
14
|
+
@cache.read(key)
|
15
|
+
end
|
16
|
+
|
17
|
+
%w(MemCacheStore FileStore MemoryStore SynchronizedMemoryStore DRbStore CompressedMemCacheStore).each do |store|
|
18
|
+
Handlers.add_handler(self, "ActiveSupport::Cache::#{store}".downcase)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/handlers/handlers.rb
CHANGED
@@ -1,4 +1,14 @@
|
|
1
1
|
module Handlers
|
2
|
+
HANDLERS = {}
|
3
|
+
|
4
|
+
def self.cache_handler_for(info)
|
5
|
+
HANDLERS[info.to_s.downcase] || HANDLERS[info.class.to_s.downcase]
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.add_handler(handler, key=nil)
|
9
|
+
HANDLERS[key || handler.cache_class.downcase] = handler
|
10
|
+
end
|
11
|
+
|
2
12
|
# creating a new cache handler is as simple as extending from the handler class,
|
3
13
|
# setting the class to use as the cache by calling cache_class("Redis")
|
4
14
|
# and then implementing the increment and get methods for that cache type.
|
@@ -6,11 +16,11 @@ module Handlers
|
|
6
16
|
# If you don't want to extend from Handler you can just create a class that implements
|
7
17
|
# increment(key), get(key) and handles?(info)
|
8
18
|
#
|
9
|
-
# Once you have a new handler make sure it is required in here and added to the Handlers list,
|
10
19
|
# you can then initialize the middleware and pass :cache=>CACHE_NAME as an option.
|
11
20
|
class Handler
|
12
21
|
def initialize(object=nil)
|
13
|
-
|
22
|
+
cache = Object.const_get(self.class.cache_class)
|
23
|
+
@cache = object.is_a?(cache) ? object : cache.new
|
14
24
|
end
|
15
25
|
|
16
26
|
def increment(key)
|
@@ -22,25 +32,15 @@ module Handlers
|
|
22
32
|
end
|
23
33
|
|
24
34
|
class << self
|
25
|
-
|
26
|
-
info.to_s.downcase == cache_class.to_s.downcase || info.is_a?(self.cache_class)
|
27
|
-
end
|
28
|
-
|
35
|
+
|
29
36
|
def cache_class(name = nil)
|
30
37
|
@cache_class = name if name
|
31
|
-
|
38
|
+
@cache_class
|
32
39
|
end
|
33
40
|
end
|
34
41
|
end
|
35
|
-
|
36
|
-
%w(redis_handler memcache_handler hash_handler).each do |handler|
|
42
|
+
|
43
|
+
%w(redis_handler memcache_handler hash_handler active_support_cache_store_handler).each do |handler|
|
37
44
|
require File.expand_path(File.dirname(__FILE__) + "/#{handler}")
|
38
|
-
end
|
39
|
-
|
40
|
-
HANDLERS = [RedisHandler, MemCacheHandler, HashHandler]
|
41
|
-
|
42
|
-
def self.cache_handler_for(info)
|
43
|
-
HANDLERS.detect{|handler| handler.handles?(info)}
|
44
|
-
end
|
45
|
-
|
45
|
+
end
|
46
46
|
end
|
data/test/test_api_throttling.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
2
1
|
require 'redis'
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
3
3
|
|
4
4
|
# To Run this test, you need to have the redis-server running.
|
5
5
|
# And you need to have rack-test gem installed: sudo gem install rack-test
|
@@ -92,4 +92,48 @@ class ApiThrottlingTest < Test::Unit::TestCase
|
|
92
92
|
end
|
93
93
|
end
|
94
94
|
|
95
|
+
context "using active support cache store" do
|
96
|
+
require 'active_support'
|
97
|
+
|
98
|
+
context "memory store" do
|
99
|
+
include BasicTests
|
100
|
+
|
101
|
+
before do
|
102
|
+
@@cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
|
103
|
+
end
|
104
|
+
|
105
|
+
def app
|
106
|
+
app = Rack::Builder.new {
|
107
|
+
use ApiThrottling, :requests_per_hour => 3, :cache=>@@cache_store
|
108
|
+
run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_cache_handler_should_be_memcache
|
113
|
+
assert_equal "Handlers::ActiveSupportCacheStoreHandler", app.to_app.instance_variable_get(:@handler).to_s
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "memcache store" do
|
118
|
+
include BasicTests
|
119
|
+
|
120
|
+
|
121
|
+
before do
|
122
|
+
@@cache_store = ActiveSupport::Cache.lookup_store(:memCache_store)
|
123
|
+
@@cache_store.clear
|
124
|
+
end
|
125
|
+
|
126
|
+
def app
|
127
|
+
app = Rack::Builder.new {
|
128
|
+
use ApiThrottling, :requests_per_hour => 3, :cache=>@@cache_store
|
129
|
+
run lambda {|env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '12'}, ["Hello World!"] ] }
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_cache_handler_should_be_memcache
|
134
|
+
assert_equal "Handlers::ActiveSupportCacheStoreHandler", app.to_app.instance_variable_get(:@handler).to_s
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
95
139
|
end
|
data/test/test_handlers.rb
CHANGED
@@ -1,39 +1,25 @@
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
2
1
|
require 'redis'
|
3
2
|
require 'memcache'
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
4
4
|
|
5
5
|
class HandlersTest < Test::Unit::TestCase
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def test_redis_should_handle_redis
|
12
|
-
assert Handlers::RedisHandler.handles?(:redis)
|
13
|
-
assert Handlers::RedisHandler.handles?('redis')
|
14
|
-
assert Handlers::RedisHandler.handles?('Redis')
|
15
|
-
assert Handlers::RedisHandler.handles?(Redis.new)
|
16
|
-
end
|
17
|
-
|
18
|
-
def test_redis_should_not_handle_memcache
|
19
|
-
assert !Handlers::RedisHandler.handles?(:memcache)
|
20
|
-
assert !Handlers::RedisHandler.handles?('memcache')
|
21
|
-
assert !Handlers::RedisHandler.handles?('MemCache')
|
22
|
-
assert !Handlers::RedisHandler.handles?(MemCache.new)
|
6
|
+
|
7
|
+
should "select redis handler" do
|
8
|
+
[:redis, 'redis', 'Redis', Redis.new].each do |key|
|
9
|
+
assert_equal Handlers::RedisHandler, Handlers.cache_handler_for(key)
|
10
|
+
end
|
23
11
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
assert !Handlers::MemCacheHandler.handles?(Redis.new)
|
12
|
+
|
13
|
+
should "select memcache handler" do
|
14
|
+
[:memcache, 'memcache', 'MemCache', MemCache.new].each do |key|
|
15
|
+
assert_equal Handlers::MemCacheHandler, Handlers.cache_handler_for(key)
|
16
|
+
end
|
30
17
|
end
|
31
18
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
assert Handlers::MemCacheHandler.handles?(MemCache.new)
|
19
|
+
should "select hash handler" do
|
20
|
+
[:hash, 'hash', 'Hash', {}].each do |key|
|
21
|
+
assert_equal Handlers::HashHandler, Handlers.cache_handler_for(key)
|
22
|
+
end
|
37
23
|
end
|
38
24
|
|
39
25
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jduff-api-throttling
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luc Castera
|
@@ -39,6 +39,7 @@ files:
|
|
39
39
|
- TODO.md
|
40
40
|
- VERSION.yml
|
41
41
|
- lib/api_throttling.rb
|
42
|
+
- lib/handlers/active_support_cache_store_handler.rb
|
42
43
|
- lib/handlers/handlers.rb
|
43
44
|
- lib/handlers/hash_handler.rb
|
44
45
|
- lib/handlers/memcache_handler.rb
|