Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
alexanderinc committed Dec 16, 2008
0 parents commit 696aff6
Show file tree
Hide file tree
Showing 8 changed files with 361 additions and 0 deletions.
4 changes: 4 additions & 0 deletions HISTORY
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
=== 1.0.0 / 2008-12-16

* First release

23 changes: 23 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Copyright (c) 2008, Kyle Banker, Alexander Interactive, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or other
dealings in this Software without prior written authorization.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
41 changes: 41 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
= ActiveResource Throttle

A throttler for ActiveResource requests.

== DESCRIPTION:

===Problem
You're writing a library to consume a RESTful web service. That service publishes
a throttle limit. So you need to throttle your requests to prevent the dreaded 503.

===Solution
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.

==INSTALL:

gem sources -a http://gems.github.com
gem install aiaio-active_resource_throttle

== USAGE:

require "active_resource_throttle"

class MyResource < ActiveResource::Base
include ActiveResourceThrottle
self.site = "http://example.com/api/"
throttle(:interval => 60, :requests => 20, :sleep_interval => 10)
end

class Person < MyResource; end
class Post < MyResource; end


1. Require activeresource_throttle.
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>.
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.

== ISSUES:

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.

Expect a threadsafe release in future versions.
14 changes: 14 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require 'rubygems'
require 'rake'
require 'rake/testtask'
require './lib/active_resource_throttle.rb'

desc 'Default: run unit tests.'
task :default => :test

test_files_pattern = 'test/**/*_test.rb'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = test_files_pattern
t.verbose = false
end
24 changes: 24 additions & 0 deletions active_resource_throttle.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- encoding: utf-8 -*-

Gem::Specification.new do |s|
s.name = "active_resource_throttle"
s.version = "1.0.0"
s.summary = "Request throttling for ActiveResource"
s.authors = ["Kyle Banker", "Alexander Interactive, Inc."]
s.date = "2008-12-16"
s.email = "knb@alexanderinteractive.com"
s.homepage = "http://github.com/aiaio/active_resource_throttle"

s.require_paths = ["lib"]
s.files = ["README.rdoc",
"Rakefile",
"HISTORY",
"LICENSE",
"lib/active_resource_throttle.rb",
"lib/active_resource_throttle/hash_ext.rb"]
s.test_files = ["test/active_resource_throttle_test.rb"]

s.has_rdoc = true
s.rdoc_options = ["--main", "README.rdoc"]
s.extra_rdoc_files = ["LICENSE", "HISTORY", "README.rdoc"]
end
134 changes: 134 additions & 0 deletions lib/active_resource_throttle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
$:.unshift File.join(File.dirname(__FILE__), "active_resource_throttle")
require "hash_ext"
module ActiveResourceThrottle
VERSION = "1.0.0"

# Add class inheritable attributes.
# See John Nunemaker's article at
# http://railstips.org/2008/6/13/a-class-instance-variable-update
module ClassInheritableAttributes
def cattr_inheritable(*args)
@cattr_inheritable_attrs ||= [:cattr_inheritable_attrs]
@cattr_inheritable_attrs += args
args.each do |arg|
class_eval %(
class << self; attr_accessor :#{arg} end
)
end
@cattr_inheritable_attrs
end

def inherited(subclass)
@cattr_inheritable_attrs.each do |inheritable_attribute|
instance_var = "@#{inheritable_attribute}"
subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
end
end
end

module ClassMethods
include ClassInheritableAttributes

# Getter method for sleep interval.
# Person.sleep_interval # => 15
def sleep_interval
@sleep_interval
end

# Getter method for throttle interval.
# Person.throttle_interval # => 60
def throttle_interval
@throttle_interval
end

# Getter method for throttle request limit.
# Person.throttle_request_limit # => 10
def throttle_request_limit
@throttle_request_limit
end

# Getter method for request history.
# Person.request_history # => [Tue Dec 16 17:35:01 UTC 2008, Tue Dec 16 17:35:03 UTC 2008]
def request_history
@request_history
end

# Sets throttling options for the given class and
# all subclasses.
# class Person < ActiveResource::Base
# throttle(:interval => 60, :requests => 15, :sleep_interval => 10)
# end
# Note that the _sleep_interval_ argument is optional. It will default
# to 5 seconds if not specified.
def throttle(options={})
options.assert_valid_keys(:interval, :requests, :sleep_interval)
options.assert_required_keys(:interval, :requests)
@throttle_interval = options[:interval]
@throttle_request_limit = options[:requests]
@sleep_interval = options[:sleep_interval] || 5
@request_history = []
end

# Interrupts connection requests only if
# throttle is engaged.
def connection_with_throttle(refresh = false)
throttle_connection_request if throttle_engaged?
connection_without_throttle(refresh)
end

protected

# This method does most of the work.
# If the request history excedes the limit,
# it sleeps for the specified interval and retries.
def throttle_connection_request
trim_request_history
while request_history.size >= throttle_request_limit do
sleep sleep_interval
trim_request_history
end
request_history << Time.now
end

# The request history is an array that stores
# a timestamp for each request. This trim method
# removes any elements occurring before
# Time.now - @throttle_interval. Thus, the number
# of elements signifies the number of requests in the allowed interval.
def trim_request_history
request_history.delete_if do |request_time|
request_time < (Time.now - throttle_interval)
end
end

# Throttle only if an interval and limit have been specified.
def throttle_engaged?
defined?(@throttle_interval) && defined?(@throttle_request_limit) &&
throttle_interval.to_i > 0 && throttle_request_limit.to_i > 0
end

end

# Callback invoked when ActiveResourceThrottle
# is included in a class. Note that the class
# must implement a *connection* class method for this
# to work (e.g, is an instance of ActiveResource::Base).
def self.included(klass)
if klass.respond_to?(:connection)
klass.instance_eval do
extend ClassMethods
cattr_inheritable :sleep_interval,
:throttle_interval,
:throttle_request_limit,
:request_history
class << klass
alias_method :connection_without_throttle, :connection
alias_method :connection, :connection_with_throttle
end
end
else
raise StandardError, "Cannot include throttle if class doesn't include a #connection class method."
end
end

end
11 changes: 11 additions & 0 deletions lib/active_resource_throttle/hash_ext.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Hash
def assert_valid_keys(*valid_keys)
unknown_keys = keys - valid_keys
raise ArgumentError, "Invalid option(s): #{unknown_keys.join(", ")}" unless unknown_keys.empty?
end

def assert_required_keys(*required_keys)
missing_keys = required_keys.select {|key| !keys.include?(key)}
raise ArgumentError, "Missing required option(s): #{missing_keys.join(", ")}" unless missing_keys.empty?
end
end
110 changes: 110 additions & 0 deletions test/active_resource_throttle_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
$:.unshift File.join(File.dirname(__FILE__), "..", "lib", "active_resource_throttle")
require "hash_ext"
require "rubygems"
require "active_resource"
require "active_resource/http_mock"
require "test/unit"
require "shoulda"
require File.join(File.dirname(__FILE__), "..", "lib", "active_resource_throttle")

puts "***"
puts "ActiveResource Throttle test suite will take some time to run."
puts "***"

class ActiveResourceThrottleTest < Test::Unit::TestCase

should "allow inclusion if #connection class method exists" do
class WillSucceed; def self.connection; end; end
assert WillSucceed.instance_eval { include(ActiveResourceThrottle) }
end

should "not allow inclusion if #conneciton class method is absent" do
class WillFail; end
assert_raises StandardError do
WillFail.instance_eval { include(ActiveResourceThrottle) }
end
end

class Resource < ActiveResource::Base; include ActiveResourceThrottle; end

should "raise an argument error on invalid keys" do
assert_raises ArgumentError, "Invalid option(s): random_key" do
Resource.instance_eval { throttle(:random_key => 'blah') }
end
end

should "raise an argument error on missing required keys" do
assert_raises ArgumentError, "Missing required option(s): requests" do
Resource.instance_eval { throttle(:interval => 20) }
end
end

class SampleResource < ActiveResource::Base
include ActiveResourceThrottle
self.throttle(:requests => 45, :interval => 10, :sleep_interval => 15)
self.site = "http://example.com"
self.element_name = "widget"
end

context "When ActiveResourceThrottle is included and #throttle method has been invoked - " do
should "set class instance variables" do

assert_equal 45, SampleResource.throttle_request_limit
end

should "set the interval for the class" do
assert_equal 10, SampleResource.throttle_interval
end

should "set the sleep interval" do
assert_equal 15, SampleResource.sleep_interval
end

end

context "Hitting the api at 45 requests per 15 seconds (with a throttle of 45/10)" do
setup do
@response_xml = [{:id => 1, :name => "Widgy Widget"}].to_xml(:root => "widgets")
ActiveResource::HttpMock.respond_to do |mock|
mock.get "/widgets.xml", {}, @response_xml
mock.get "/sprockets.xml", {}, @response_xml
mock.get "/fridgets.xml", {}, @response_xml
end
end

should "require more than 20 seconds to make over 90 requests" do
@start_time = Time.now
1.upto(91) { SampleResource.find :all }
@end_time = Time.now
assert @end_time - @start_time > 20
end

context "with multiple subclasses" do
setup do
class SubSampleResource1 < SampleResource
self.element_name = "sprocket"
end
class SubSampleResource2 < SampleResource
self.element_name = "fridget"
end
end

should "have the same settings as superclass" do
assert_equal 10, SubSampleResource1.throttle_interval
assert_equal 45, SubSampleResource1.throttle_request_limit
end

should "require more than 20 seconds to make over 90 requests" do
@start_time = Time.now
1.upto(31) do
SampleResource.find :all
SubSampleResource1.find :all
SubSampleResource2.find :all
end
@end_time = Time.now
assert @end_time - @start_time > 20
end
end
end

end

0 comments on commit 696aff6

Please sign in to comment.