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 +4 -0
- data/LICENSE +23 -0
- data/README.rdoc +41 -0
- data/Rakefile +14 -0
- data/lib/active_resource_throttle.rb +141 -0
- data/lib/active_resource_throttle/hash_ext.rb +16 -0
- data/test/active_resource_throttle_test.rb +110 -0
- metadata +64 -0
data/HISTORY
ADDED
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.
|
data/README.rdoc
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|