active_resource_throttle 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY ADDED
@@ -0,0 +1,4 @@
1
+ === 1.0.0 / 2008-12-16
2
+
3
+ * First release
4
+
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2008, Kyle Banker, Alexander Interactive, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ Except as contained in this notice, the name(s) of the above copyright holders
14
+ shall not be used in advertising or otherwise to promote the sale, use or other
15
+ dealings in this Software without prior written authorization.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
@@ -0,0 +1,41 @@
1
+ = ActiveResource Throttle
2
+
3
+ A rate limiter for ActiveResource requests.
4
+
5
+ == DESCRIPTION:
6
+
7
+ ===Problem
8
+ You're writing a library to consume a RESTful web service. That service publishes
9
+ a throttle limit. So you need to throttle your requests to prevent the dreaded 503.
10
+
11
+ ===Solution
12
+ ActiveResource Throttle adds request throttling to ActiveResource. Specify the limits in your ActiveResource base class, and no longer will your client code have to worry about the number and frequency of its requests.
13
+
14
+ ==INSTALL:
15
+
16
+ gem sources -a http://gems.github.com
17
+ gem install aiaio-active_resource_throttle
18
+
19
+ == USAGE:
20
+
21
+ require "active_resource_throttle"
22
+
23
+ class MyResource < ActiveResource::Base
24
+ include ActiveResourceThrottle
25
+ self.site = "http://example.com/api/"
26
+ throttle(:interval => 60, :requests => 20, :sleep_interval => 10)
27
+ end
28
+
29
+ class Person < MyResource; end
30
+ class Post < MyResource; end
31
+
32
+
33
+ 1. Require activeresource_throttle.
34
+ 2. Include ActiveResourceThrottle in the ActiveResource class. If you're creating a library to access several resources, <b>it's necessary to create a generic base class for the api</b> you're accessing. Specify the site, login credentials, and throttle, and then subclass the base class for the various resources. Note that <b>the throttle will work across subclasses</b>.
35
+ 3. Invoke the #throttle class method with the required options *interval* and *requests*. You may also specify a *sleep_interval*. The settings in the example code above will allow for a maximum of 20 requests per minute. When that limit is reached, requests will be paused for 10 seconds.
36
+
37
+ == ISSUES:
38
+
39
+ ActiveResource Throttle will not work properly across multiple instances of ActiveResource (e.g., in a Rails application with more than one Mongrel). At the moment, it should be used in single-process scripts. Also, the gem is not yet threadsafe.
40
+
41
+ Expect a threadsafe release in future versions.
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require './lib/active_resource_throttle.rb'
5
+
6
+ desc 'Default: run unit tests.'
7
+ task :default => :test
8
+
9
+ test_files_pattern = 'test/**/*_test.rb'
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << 'lib'
12
+ t.pattern = test_files_pattern
13
+ t.verbose = false
14
+ end
@@ -0,0 +1,141 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), "active_resource_throttle")
2
+ require "hash_ext"
3
+ module ActiveResourceThrottle
4
+ VERSION = "1.0.0"
5
+
6
+ # Add class inheritable attributes.
7
+ # See John Nunemaker's article at
8
+ # http://railstips.org/2008/6/13/a-class-instance-variable-update
9
+ module ClassInheritableAttributes
10
+ def cattr_inheritable(*args)
11
+ @cattr_inheritable_attrs ||= [:cattr_inheritable_attrs]
12
+ @cattr_inheritable_attrs += args
13
+ args.each do |arg|
14
+ class_eval %(
15
+ class << self; attr_accessor :#{arg} end
16
+ )
17
+ end
18
+ @cattr_inheritable_attrs
19
+ end
20
+
21
+ def inherited(subclass)
22
+ @cattr_inheritable_attrs.each do |inheritable_attribute|
23
+ instance_var = "@#{inheritable_attribute}"
24
+ subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
25
+ end
26
+ end
27
+ end
28
+
29
+ module ClassMethods
30
+ include ClassInheritableAttributes
31
+
32
+ # Getter method for sleep interval.
33
+ # Person.sleep_interval # => 15
34
+ def sleep_interval
35
+ @sleep_interval
36
+ end
37
+
38
+ # Getter method for throttle interval.
39
+ # Person.throttle_interval # => 60
40
+ def throttle_interval
41
+ @throttle_interval
42
+ end
43
+
44
+ # Getter method for throttle request limit.
45
+ # Person.throttle_request_limit # => 10
46
+ def throttle_request_limit
47
+ @throttle_request_limit
48
+ end
49
+
50
+ # Getter method for request history.
51
+ # Person.request_history # => [Tue Dec 16 17:35:01 UTC 2008, Tue Dec 16 17:35:03 UTC 2008]
52
+ def request_history
53
+ @request_history
54
+ end
55
+
56
+ # Sets throttling options for the given class and
57
+ # all subclasses.
58
+ # class Person < ActiveResource::Base
59
+ # throttle(:interval => 60, :requests => 15, :sleep_interval => 10)
60
+ # end
61
+ # Note that the _sleep_interval_ argument is optional. It will default
62
+ # to 5 seconds if not specified.
63
+ def throttle(options={})
64
+ options.assert_valid_keys(:interval, :requests, :sleep_interval)
65
+ options.assert_required_keys(:interval, :requests)
66
+ @throttle_interval = options[:interval]
67
+ @throttle_request_limit = options[:requests]
68
+ @sleep_interval = options[:sleep_interval] || 5
69
+ @request_history = []
70
+ end
71
+
72
+ # Interrupts connection requests only if
73
+ # throttle is engaged.
74
+ def connection_with_throttle(refresh = false)
75
+ throttle_connection_request if throttle_engaged? && base_class?
76
+ connection_without_throttle(refresh)
77
+ end
78
+
79
+ protected
80
+
81
+ # This method does most of the work.
82
+ # If the request history excedes the limit,
83
+ # it sleeps for the specified interval and retries.
84
+ def throttle_connection_request
85
+ trim_request_history
86
+ while request_history.size >= throttle_request_limit do
87
+ sleep sleep_interval
88
+ trim_request_history
89
+ end
90
+ request_history << Time.now
91
+ end
92
+
93
+ # The request history is an array that stores
94
+ # a timestamp for each request. This trim method
95
+ # removes any elements occurring before
96
+ # Time.now - @throttle_interval. Thus, the number
97
+ # of elements signifies the number of requests in the allowed interval.
98
+ def trim_request_history
99
+ request_history.delete_if do |request_time|
100
+ request_time < (Time.now - throttle_interval)
101
+ end
102
+ end
103
+
104
+ # Throttle only if an interval and limit have been specified.
105
+ def throttle_engaged?
106
+ defined?(@throttle_interval) && defined?(@throttle_request_limit) &&
107
+ throttle_interval.to_i > 0 && throttle_request_limit.to_i > 0
108
+ end
109
+
110
+ # Is this the class from which a connection will originate?
111
+ # See ActiveResource::Base.connection method for details.
112
+ def base_class?
113
+ defined?(@connection) || superclass == Object
114
+ end
115
+
116
+
117
+ end
118
+
119
+ # Callback invoked when ActiveResourceThrottle
120
+ # is included in a class. Note that the class
121
+ # must implement a *connection* class method for this
122
+ # to work (e.g, is an instance of ActiveResource::Base).
123
+ def self.included(klass)
124
+ if klass.respond_to?(:connection)
125
+ klass.instance_eval do
126
+ extend ClassMethods
127
+ cattr_inheritable :sleep_interval,
128
+ :throttle_interval,
129
+ :throttle_request_limit,
130
+ :request_history
131
+ class << klass
132
+ alias_method :connection_without_throttle, :connection
133
+ alias_method :connection, :connection_with_throttle
134
+ end
135
+ end
136
+ else
137
+ raise StandardError, "Cannot include throttle if class doesn't include a #connection class method."
138
+ end
139
+ end
140
+
141
+ end
@@ -0,0 +1,16 @@
1
+ module Hash::ValidKeys
2
+ def assert_valid_keys(*valid_keys)
3
+ unknown_keys = keys - valid_keys
4
+ raise ArgumentError, "Invalid option(s): #{unknown_keys.join(", ")}" unless unknown_keys.empty?
5
+ end
6
+ end
7
+
8
+ class Hash
9
+ include ValidKeys unless defined?(ActiveSupport)
10
+
11
+ def assert_required_keys(*required_keys)
12
+ missing_keys = required_keys.select {|key| !keys.include?(key)}
13
+ raise ArgumentError, "Missing required option(s): #{missing_keys.join(", ")}" unless missing_keys.empty?
14
+ end
15
+ end
16
+
@@ -0,0 +1,110 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib", "active_resource_throttle")
2
+ require "hash_ext"
3
+ require "rubygems"
4
+ require "active_resource"
5
+ require "active_resource/http_mock"
6
+ require "test/unit"
7
+ require "shoulda"
8
+ require File.join(File.dirname(__FILE__), "..", "lib", "active_resource_throttle")
9
+
10
+ puts "***"
11
+ puts "ActiveResource Throttle test suite will take some time to run."
12
+ puts "***"
13
+
14
+ class ActiveResourceThrottleTest < Test::Unit::TestCase
15
+
16
+ should "allow inclusion if #connection class method exists" do
17
+ class WillSucceed; def self.connection; end; end
18
+ assert WillSucceed.instance_eval { include(ActiveResourceThrottle) }
19
+ end
20
+
21
+ should "not allow inclusion if #conneciton class method is absent" do
22
+ class WillFail; end
23
+ assert_raises StandardError do
24
+ WillFail.instance_eval { include(ActiveResourceThrottle) }
25
+ end
26
+ end
27
+
28
+ class Resource < ActiveResource::Base; include ActiveResourceThrottle; end
29
+
30
+ should "raise an argument error on invalid keys" do
31
+ assert_raises ArgumentError, "Invalid option(s): random_key" do
32
+ Resource.instance_eval { throttle(:random_key => 'blah') }
33
+ end
34
+ end
35
+
36
+ should "raise an argument error on missing required keys" do
37
+ assert_raises ArgumentError, "Missing required option(s): requests" do
38
+ Resource.instance_eval { throttle(:interval => 20) }
39
+ end
40
+ end
41
+
42
+ class SampleResource < ActiveResource::Base
43
+ include ActiveResourceThrottle
44
+ self.throttle(:requests => 45, :interval => 10, :sleep_interval => 15)
45
+ self.site = "http://example.com"
46
+ self.element_name = "widget"
47
+ end
48
+
49
+ context "When ActiveResourceThrottle is included and #throttle method has been invoked - " do
50
+ should "set class instance variables" do
51
+
52
+ assert_equal 45, SampleResource.throttle_request_limit
53
+ end
54
+
55
+ should "set the interval for the class" do
56
+ assert_equal 10, SampleResource.throttle_interval
57
+ end
58
+
59
+ should "set the sleep interval" do
60
+ assert_equal 15, SampleResource.sleep_interval
61
+ end
62
+
63
+ end
64
+
65
+ context "Hitting the api at 45 requests per 15 seconds (with a throttle of 45/10)" do
66
+ setup do
67
+ @response_xml = [{:id => 1, :name => "Widgy Widget"}].to_xml(:root => "widgets")
68
+ ActiveResource::HttpMock.respond_to do |mock|
69
+ mock.get "/widgets.xml", {}, @response_xml
70
+ mock.get "/sprockets.xml", {}, @response_xml
71
+ mock.get "/fridgets.xml", {}, @response_xml
72
+ end
73
+ end
74
+
75
+ should "require more than 20 seconds to make over 90 requests" do
76
+ @start_time = Time.now
77
+ 1.upto(91) { SampleResource.find :all }
78
+ @end_time = Time.now
79
+ assert @end_time - @start_time > 20
80
+ end
81
+
82
+ context "with multiple subclasses" do
83
+ setup do
84
+ class SubSampleResource1 < SampleResource
85
+ self.element_name = "sprocket"
86
+ end
87
+ class SubSampleResource2 < SampleResource
88
+ self.element_name = "fridget"
89
+ end
90
+ end
91
+
92
+ should "have the same settings as superclass" do
93
+ assert_equal 10, SubSampleResource1.throttle_interval
94
+ assert_equal 45, SubSampleResource1.throttle_request_limit
95
+ end
96
+
97
+ should "require more than 20 seconds to make over 90 requests" do
98
+ @start_time = Time.now
99
+ 1.upto(31) do
100
+ SampleResource.find :all
101
+ SubSampleResource1.find :all
102
+ SubSampleResource2.find :all
103
+ end
104
+ @end_time = Time.now
105
+ assert @end_time - @start_time > 20
106
+ end
107
+ end
108
+ end
109
+
110
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_resource_throttle
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kyle Banker
8
+ - Alexander Interactive, Inc.
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2008-12-16 00:00:00 -05:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description:
18
+ email: knb@alexanderinteractive.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files:
24
+ - LICENSE
25
+ - HISTORY
26
+ - README.rdoc
27
+ files:
28
+ - README.rdoc
29
+ - Rakefile
30
+ - HISTORY
31
+ - LICENSE
32
+ - lib/active_resource_throttle.rb
33
+ - lib/active_resource_throttle/hash_ext.rb
34
+ has_rdoc: true
35
+ homepage: http://github.com/aiaio/active_resource_throttle
36
+ licenses: []
37
+
38
+ post_install_message:
39
+ rdoc_options:
40
+ - --main
41
+ - README.rdoc
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ requirements: []
57
+
58
+ rubyforge_project:
59
+ rubygems_version: 1.3.5
60
+ signing_key:
61
+ specification_version: 3
62
+ summary: A throttler for ActiveResource requests.
63
+ test_files:
64
+ - test/active_resource_throttle_test.rb