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 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
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
3
  :minor: 2
4
- :patch: 0
4
+ :patch: 1
@@ -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
@@ -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
- @cache = object.is_a?(self.class.cache_class) ? object : self.class.cache_class.new
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
- def handles?(info)
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
- Object.const_get(@cache_class) if @cache_class
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
@@ -9,5 +9,7 @@ module Handlers
9
9
  def get(key)
10
10
  @cache[key]
11
11
  end
12
+
13
+ Handlers.add_handler self
12
14
  end
13
15
  end
@@ -1,4 +1,3 @@
1
- require 'memcache'
2
1
  module Handlers
3
2
  class MemCacheHandler < Handler
4
3
  cache_class "MemCache"
@@ -10,6 +9,7 @@ module Handlers
10
9
  def get(key)
11
10
  @cache.get(key)
12
11
  end
13
-
12
+
13
+ Handlers.add_handler self
14
14
  end
15
15
  end
@@ -1,4 +1,3 @@
1
- require 'redis'
2
1
  module Handlers
3
2
  class RedisHandler < Handler
4
3
  cache_class "Redis"
@@ -10,5 +9,7 @@ module Handlers
10
9
  def get(key)
11
10
  @cache[key]
12
11
  end
12
+
13
+ Handlers.add_handler self
13
14
  end
14
15
  end
@@ -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
@@ -1,5 +1,5 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
1
  require 'memcache'
2
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
3
3
 
4
4
  class TestApiThrottlingMemcache < Test::Unit::TestCase
5
5
  include Rack::Test::Methods
@@ -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
- def setup
8
-
9
- end
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
- def test_memcache_should_not_handle_redis
26
- assert !Handlers::MemCacheHandler.handles?(:redis)
27
- assert !Handlers::MemCacheHandler.handles?('redis')
28
- assert !Handlers::MemCacheHandler.handles?('Redis')
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
- def test_memcache_should_handle_memcache
33
- assert Handlers::MemCacheHandler.handles?(:memcache)
34
- assert Handlers::MemCacheHandler.handles?('memcache')
35
- assert Handlers::MemCacheHandler.handles?('MemCache')
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.0
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