Permalink
Browse files

Add request governor.

  • Loading branch information...
1 parent 396b058 commit 4467be47f1030e504bd1a34610643c9d7b208c5c @benbjohnson committed Jan 28, 2011
Showing with 421 additions and 5 deletions.
  1. +2 −0 .gitignore
  2. +2 −0 CHANGELOG
  3. +2 −0 Gemfile
  4. +26 −0 Gemfile.lock
  5. +5 −5 README.md
  6. +60 −0 Rakefile
  7. +58 −0 lib/slow_web.rb
  8. +25 −0 lib/slow_web/ext/net_http.rb
  9. +75 −0 lib/slow_web/limit.rb
  10. +3 −0 lib/slow_web/version.rb
  11. +1 −0 lib/slowweb.rb
  12. +22 −0 slowweb.gemspec
  13. +40 −0 spec/limit_spec.rb
  14. +47 −0 spec/net_http_spec.rb
  15. +38 −0 spec/slow_web_spec.rb
  16. +15 −0 spec/spec_helper.rb
View
2 .gitignore
@@ -0,0 +1,2 @@
+.yardoc
+*.gem
View
2 CHANGELOG
@@ -0,0 +1,2 @@
+v0.1.0
+* Initial release.
View
2 Gemfile
@@ -0,0 +1,2 @@
+source :gemcutter
+gemspec
View
26 Gemfile.lock
@@ -0,0 +1,26 @@
+PATH
+ remote: .
+ specs:
+ slowweb (0.1.0)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ diff-lcs (1.1.2)
+ fakeweb (1.3.0)
+ rspec (2.4.0)
+ rspec-core (~> 2.4.0)
+ rspec-expectations (~> 2.4.0)
+ rspec-mocks (~> 2.4.0)
+ rspec-core (2.4.0)
+ rspec-expectations (2.4.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.4.0)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ fakeweb (~> 1.3.0)
+ rspec (~> 2.4.0)
+ slowweb!
View
10 README.md
@@ -1,5 +1,5 @@
-Slow Web - A Request Governor
-=============================
+Slow Web - An HTTP Request Governor
+===================================
## DESCRIPTION
@@ -22,10 +22,10 @@ To install Slow Web, simply install the gem:
And specify the domain to limit.
require 'slowweb'
- SlowWeb.limit('github.com', 60, 3600)
+ SlowWeb.limit('github.com', 10, 60)
-This restricts the `github.com` domain to only allowing `60` requests every
-`3600` seconds (or one hour).
+This restricts the `github.com` domain to only allowing `10` requests every
+`60` seconds (or one minute).
## CONTRIBUTE
View
60 Rakefile
@@ -0,0 +1,60 @@
+lib = File.expand_path('lib', File.dirname(__FILE__))
+$:.unshift lib unless $:.include?(lib)
+
+require 'rubygems'
+require 'rake'
+require 'rake/rdoctask'
+require 'rake/testtask'
+require 'slowweb'
+
+#############################################################################
+#
+# Standard tasks
+#
+#############################################################################
+
+Rake::TestTask.new(:test) do |test|
+ test.libs << 'lib' << 'test'
+ test.pattern = 'test/**/test_*.rb'
+ test.verbose = true
+end
+
+Rake::RDocTask.new do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = "SlowWeb #{SlowWeb::VERSION}"
+ rdoc.rdoc_files.include('README*')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+task :console do
+ sh "irb -rubygems -r ./lib/slowweb.rb"
+end
+
+
+#############################################################################
+#
+# Packaging tasks
+#
+#############################################################################
+
+task :release do
+ puts ""
+ print "Are you sure you want to relase SlowWeb #{SlowWeb::VERSION}? [y/N] "
+ exit unless STDIN.gets.index(/y/i) == 0
+
+ unless `git branch` =~ /^\* master$/
+ puts "You must be on the master branch to release!"
+ exit!
+ end
+
+ # Build gem and upload
+ sh "gem build slowweb.gemspec"
+ sh "gem push slowweb-#{SlowWeb::VERSION}.gem"
+ sh "rm slowweb-#{SlowWeb::VERSION}.gem"
+
+ # Commit
+ sh "git commit --allow-empty -a -m 'v#{SlowWeb::VERSION}'"
+ sh "git tag v#{SlowWeb::VERSION}"
+ sh "git push origin master"
+ sh "git push origin v#{SlowWeb::VERSION}"
+end
View
58 lib/slow_web.rb
@@ -0,0 +1,58 @@
+require 'slow_web/version'
+require 'slow_web/limit'
+require 'slow_web/ext/net_http'
+
+class SlowWeb
+ ##############################################################################
+ # Static Initialization
+ ##############################################################################
+
+ # A look up of limits by host.
+ @limits = {}
+
+
+ ##############################################################################
+ # Static Methods
+ ##############################################################################
+
+ # Limits the number of requests that can occur within a specified number of
+ # seconds.
+ #
+ # @param [String] host the host to restrict.
+ # @param [Fixnum] count the number of requests that can occur within a time period.
+ # @param [Fixnum] period the number of seconds in the time period.
+ #
+ # @return [SlowWeb::Limit] the limit object.
+ def self.limit(host, count, period)
+ raise "Limit already exists for this host: #{host}" if @limits[host]
+
+ limit = Limit.new(host, count, period)
+ @limits[host] = limit
+ return limit
+ end
+
+ # Retrieves the limit object for a given host.
+ #
+ # @param [String] host the host associated with the limit.
+ #
+ # @return [SlowWeb::Limit] the limit object.
+ def self.get_limit(host)
+ return @limits[host]
+ end
+
+ # A flag stating if the limit for a given host has been exceeded.
+ #
+ # @param [String] host the host that is being limited.
+ #
+ # @return [Boolean] a flag stating if the limit has been exceeded.
+ def self.limit_exceeded?(host)
+ limit = @limits[host]
+ return !limit.nil? && limit.exceeded?
+ end
+
+
+ # Removes all limits.
+ def self.reset
+ @limits = {}
+ end
+end
View
25 lib/slow_web/ext/net_http.rb
@@ -0,0 +1,25 @@
+require 'net/http'
+require 'net/https'
+
+module Net
+ class HTTP
+ def request_with_slowweb(request, body = nil, &block)
+ host = self.address
+ limit = SlowWeb.get_limit(host)
+
+ # Wait until the request limit is no longer exceeded
+ while limit.exceeded?
+ sleep 1
+ end
+
+ # Add request to limiter
+ limit.add_request(request)
+
+ # Continue with the original request
+ request_without_slowweb(request, body, &block)
+ end
+
+ alias_method :request_without_slowweb, :request
+ alias_method :request, :request_with_slowweb
+ end
+end
View
75 lib/slow_web/limit.rb
@@ -0,0 +1,75 @@
+require 'slow_web/version'
+
+class SlowWeb
+ class Limit
+ ############################################################################
+ # Constructor
+ ############################################################################
+
+ # @param [String] host the host to restrict.
+ # @param [Fixnum] count the number of requests that can occur within a time
+ # period.
+ # @param [Fixnum] period the number of seconds in the time period.
+ def initialize(host, count, period)
+ @host = host
+ @count = count
+ @period = period
+
+ @requests = []
+ end
+
+
+ ############################################################################
+ # Public Attributes
+ ############################################################################
+
+ # The host to restrict.
+ attr_accessor :host
+
+ # The number of requests that are allowed within the time period.
+ attr_accessor :count
+
+ # The number of seconds in the time period.
+ attr_accessor :period
+
+ # The number of requests that have occurred within the current period.
+ def current_request_count
+ normalize_requests()
+ return @requests.length
+ end
+
+ # A flag stating if the number of requests within the period has been met.
+ def exceeded?
+ return current_request_count >= count
+ end
+
+
+ ############################################################################
+ # Public Methods
+ ############################################################################
+
+ # Adds a request that is associated with this limit
+ #
+ # @param [Net::HTTPRequest] request the request associated with this limit.
+ def add_request(request)
+ @requests << {:obj => request, :time => Time.now}
+ normalize_requests()
+ nil
+ end
+
+
+ ############################################################################
+ # Private Methods
+ ############################################################################
+
+ private
+
+ # Removes all items in the request list that are outside of the current
+ # period.
+ def normalize_requests
+ @requests = @requests.find_all do |request|
+ (Time.now-request[:time]) < period
+ end
+ end
+ end
+end
View
3 lib/slow_web/version.rb
@@ -0,0 +1,3 @@
+class SlowWeb
+ VERSION = '0.1.0'
+end
View
1 lib/slowweb.rb
@@ -0,0 +1 @@
+require 'slow_web'
View
22 slowweb.gemspec
@@ -0,0 +1,22 @@
+# -*- encoding: utf-8 -*-
+lib = File.expand_path('../lib/', __FILE__)
+$:.unshift lib unless $:.include?(lib)
+
+require 'slow_web/version'
+
+Gem::Specification.new do |s|
+ s.name = 'slowweb'
+ s.version = SlowWeb::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ['Ben Johnson']
+ s.email = ['benbjohnson@yahoo.com']
+ s.homepage = 'http://github.com/benbjohnson/slowweb'
+ s.summary = 'An HTTP Request Governor'
+
+ s.add_development_dependency('rspec', '~> 2.4.0')
+ s.add_development_dependency('fakeweb', '~> 1.3.0')
+
+ s.test_files = Dir.glob('test/**/*')
+ s.files = Dir.glob('lib/**/*') + %w(README.md)
+ s.require_path = 'lib'
+end
View
40 spec/limit_spec.rb
@@ -0,0 +1,40 @@
+require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper')
+
+describe SlowWeb::Limit do
+ ##############################################################################
+ # Setup
+ ##############################################################################
+
+ before do
+ @limit = SlowWeb::Limit.new('github.com', 10, 60)
+ end
+
+ after do
+ @limit = nil
+ end
+
+
+ ##############################################################################
+ # Tests
+ ##############################################################################
+
+ it 'should increase the request count when a request is added' do
+ @limit.add_request({})
+ @limit.current_request_count.should == 1
+ end
+
+ it 'should show the limit exceeded if request count is above threshold' do
+ @limit.count = 3
+ @limit.add_request({})
+ @limit.add_request({})
+ @limit.add_request({})
+ @limit.should be_exceeded
+ end
+
+ it 'should not show the limit exceeded if request count is below threshold' do
+ @limit.count = 3
+ @limit.add_request({})
+ @limit.add_request({})
+ @limit.should_not be_exceeded
+ end
+end
View
47 spec/net_http_spec.rb
@@ -0,0 +1,47 @@
+require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper')
+
+describe Net::HTTP do
+ ##############################################################################
+ # Setup
+ ##############################################################################
+
+ before do
+ SlowWeb.reset
+ FakeWeb.allow_net_connect = false
+ end
+
+
+ ##############################################################################
+ # Tests
+ ##############################################################################
+
+ it 'should show limit exceeded' do
+ FakeWeb.register_uri(:get, 'http://github.com', :body => 'foo')
+ SlowWeb.limit('github.com', 3, 60)
+ open('http://github.com')
+ open('http://github.com')
+ open('http://github.com')
+ SlowWeb.limit_exceeded?('github.com').should be_true
+ end
+
+ it 'should not show limit exceeded after waiting' do
+ FakeWeb.register_uri(:get, 'http://github.com', :body => 'foo')
+ SlowWeb.limit('github.com', 3, 1)
+ open('http://github.com')
+ open('http://github.com')
+ open('http://github.com')
+ sleep(1)
+ SlowWeb.limit_exceeded?('github.com').should be_false
+ end
+
+ it 'should wait for additional requests' do
+ FakeWeb.register_uri(:get, 'http://github.com', :body => 'foo')
+ SlowWeb.limit('github.com', 3, 1)
+ t = Time.now
+ open('http://github.com')
+ open('http://github.com')
+ open('http://github.com')
+ open('http://github.com')
+ (Time.now-t).should > 1
+ end
+end
View
38 spec/slow_web_spec.rb
@@ -0,0 +1,38 @@
+require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper')
+
+describe SlowWeb do
+ ##############################################################################
+ # Setup
+ ##############################################################################
+
+ before do
+ SlowWeb.reset
+ end
+
+
+ ##############################################################################
+ # Tests
+ ##############################################################################
+
+ it 'should add limit' do
+ SlowWeb.limit('github.com', 10, 60)
+ limit = SlowWeb.get_limit('github.com');
+ limit.host.should == 'github.com'
+ limit.count.should == 10
+ limit.period.should == 60
+ end
+
+ it 'should error when limiting the same host twice' do
+ SlowWeb.limit('github.com', 10, 60)
+ lambda {SlowWeb.limit('github.com', 10, 60)}.should raise_error('Limit already exists for this host: github.com')
+ end
+
+ it 'should show limit exceeded' do
+ SlowWeb.limit('github.com', 3, 60)
+ limit = SlowWeb.get_limit('github.com');
+ limit.add_request({})
+ limit.add_request({})
+ limit.add_request({})
+ SlowWeb.limit_exceeded?('github.com').should be_true
+ end
+end
View
15 spec/spec_helper.rb
@@ -0,0 +1,15 @@
+dir = File.dirname(File.expand_path(__FILE__))
+$:.unshift(File.join(dir, '..', 'lib'))
+$:.unshift(dir)
+
+require 'rubygems'
+require 'bundler/setup'
+require 'open-uri'
+require 'rspec'
+require 'fakeweb'
+require 'slowweb'
+
+# Configure RSpec
+Rspec.configure do |c|
+ c.mock_with :rspec
+end

0 comments on commit 4467be4

Please sign in to comment.