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 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