Skip to content

Added Pacemaker module. #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ group :development, :test do
gem 'rspec'
gem 'debugger'
gem 'factory_girl'
gem 'timecop'
end
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ GEM
rspec-expectations (2.11.2)
diff-lcs (~> 1.1.3)
rspec-mocks (2.11.2)
timecop (0.5.4)
typhoeus (0.5.1)
ethon (= 0.5.3)

Expand All @@ -46,4 +47,5 @@ DEPENDENCIES
factory_girl
json
rspec
timecop
typhoeus (= 0.5.1)
21 changes: 21 additions & 0 deletions lib/pacemaker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# mixin to ApiReader to automatically pace calls to stay within rate limits
module Pacemaker
def paced_load_feed
data = old_load_feed
pace = sleep_pace
puts "Sleeping #{pace}" if pace > 0
sleep pace
data
end

def sleep_pace
remaining = rate_limit_remaining
return 0 if remaining > 10

now = Time.now
next_hour = now + (60 * 60)
top_of_next_hour = Time.new(next_hour.year, next_hour.month, next_hour.day, next_hour.hour)
seconds_until_reset = top_of_next_hour - now
(seconds_until_reset / (remaining + 1)).to_i + 10
end
end
2 changes: 2 additions & 0 deletions lib/the_city_admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

require File.dirname(__FILE__) + '/common.rb'

require File.dirname(__FILE__) + '/pacemaker.rb'


# This class is meant to be a wrapper TheCity Admin API (OnTheCity.org).
module TheCity
Expand Down
10 changes: 10 additions & 0 deletions spec/factories/rate_header.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module TheCity
class RateHeader < Struct.new(:ip_limit, :ip_remaining, :account_limit, :account_remaining)
def header_raw
["X-City-RateLimit-Limit-By-Ip: #{ip_limit}",
"X-City-RateLimit-Remaining-By-Ip: #{ip_remaining}",
"X-City-RateLimit-Limit-By-Account: #{account_limit}",
"X-City-RateLimit-Remaining-By-Account: #{account_remaining}"].join("\r\n")
end
end
end
15 changes: 6 additions & 9 deletions spec/readers/api_reader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,17 @@
describe TheCity::ApiReader do

it "should include City headers" do
headers = "X-City-RateLimit-Limit-By-Ip: 2200\r\nX-City-RateLimit-Remaining-By-Ip: 2199\r\n"
TheCity.stub(:admin_request).and_return(TheCityResponse.new(200, {}.to_json, headers))
rate_headers = TheCity::RateHeader.new(2200, 2199)
TheCity.stub(:admin_request).and_return(TheCityResponse.new(200, {}.to_json, rate_headers.header_raw))
reader = TheCity::ApiReader.new
reader.load_feed.should == {}
reader.headers['X-City-RateLimit-Limit-By-Ip'].should == '2200'
reader.headers['X-City-RateLimit-Remaining-By-Ip'].should == '2199'
reader.headers['X-City-RateLimit-Limit-By-Ip'.downcase].should == '2200'
reader.headers['X-City-RateLimit-Remaining-By-Ip'.downcase].should == '2199'
end

it "should include convenience methods for reading rate limit data" do
headers = ["X-City-RateLimit-Limit-By-Ip: 2000",
"X-City-RateLimit-Remaining-By-Ip: 1987",
"X-City-RateLimit-Limit-By-Account: 3000",
"X-City-RateLimit-Remaining-By-Account: 1561"].join("\r\n")
TheCity.stub(:admin_request).and_return(TheCityResponse.new(200, {}.to_json, headers))
rate_headers = TheCity::RateHeader.new(2000, 1987, 3000, 1561)
TheCity.stub(:admin_request).and_return(TheCityResponse.new(200, {}.to_json, rate_headers.header_raw))
reader = TheCity::ApiReader.new
reader.load_feed.should == {}
reader.rate_limit.should == 2000
Expand Down
46 changes: 46 additions & 0 deletions spec/readers/pacemaker_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require File.dirname(__FILE__) + '/../spec_helper'

def do_test(opts={})
rate_headers = TheCity::RateHeader.new(0, opts[:remaining_calls], 0, opts[:remaining_calls])
TheCity.stub(:admin_request).and_return(TheCityResponse.new(200, {}.to_json, rate_headers.header_raw))

reader = TheCity::ApiReader.new
reader.extend Pacemaker

Timecop.freeze(2012, 12, 05, 12, opts[:now_min], opts[:now_sec]) do
reader.load_feed.should == {}
reader.sleep_pace.should == opts[:expected_pace]
end
end

describe Pacemaker do
it 'should not sleep if sleep time is less than 1 second' do
do_test(:now_min => 1, :now_sec => 0, :remaining_calls => 9000, :expected_pace => 0)
end

# perhaps should be configurable. or should support knowing an estimated number of calls
# remaining. if there are 50 calls left and 59 minutes left in the hour, but i only need
# to make 4 calls, no reason to sleep at all. the worst case scenarios here force sleeping
# when it's not really desired.
it 'should not sleep if remaining calls is more than 10' do
do_test(:now_min => 1, :now_sec => 0, :remaining_calls => 11, :expected_pace => 0)
end

# the buffer is required so you don't make the first new call of the hour anticipating it to work
# when the client and server clocks are not in sync and then it fails to work
it 'should sleep for the rest of the hour (plus 10 second buffer) if no more calls are left' do
do_test(:now_min => 1, :now_sec => 0, :remaining_calls => 0, :expected_pace => (59*60)+10)
end

it 'should sleep for one second (plus buffer) with 2 seconds to go and 1 call left' do
do_test(:now_min => 59, :now_sec => 58, :remaining_calls => 1, :expected_pace => 1 + 10)
end

it 'should sleep for 30 seconds with 1 minute to go and 1 call left' do
do_test(:now_min => 59, :now_sec => 0, :remaining_calls => 1, :expected_pace => 30 + 10)
end

it 'should sleep for 15 seconds with 1 minute to go and 3 calls left' do
do_test(:now_min => 59, :now_sec => 0, :remaining_calls => 3, :expected_pace => 15 + 10)
end
end
7 changes: 6 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@
require 'rspec'
require 'ruby-debug'
require 'date'
require 'timecop'

require 'factory_girl'
Dir.glob(File.dirname(__FILE__) + "/factories/*").each { |f| require f }


TheCityResponse = Struct.new(:code, :body, :headers)
class TheCityResponse < Struct.new(:code, :body, :header_raw)
def headers
Typhoeus::Response::Header.new(header_raw)
end
end

RSpec.configure do |config|
config.tty = true
Expand Down