jduff-api-throttling 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|