Skip to content

Commit

Permalink
Refactor Twitter::RateLimit class to be non-global
Browse files Browse the repository at this point in the history
The global data being overwritten when there were multiple clients
running in parallel. Now, each client has its own Twitter::RateLimit
instance, as does each Twitter::Error object.

Closes #289.
  • Loading branch information
sferik committed Jul 17, 2012
1 parent 1702f05 commit 6e9da0d
Show file tree
Hide file tree
Showing 19 changed files with 125 additions and 83 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ Here are some fun facts about the 3.0 release:

* The entire library is implemented in just 2,254 lines of code
* With over 5,000 lines of specs, the spec-to-code ratio is well over 2:1
* The spec suite contains 653 examples and runs in under 2 seconds on a MacBook
* The spec suite contains 658 examples and runs in under 2 seconds on a MacBook
* This project has 100% C0 code coverage (the tests execute every line of
source code at least once)
* At the time of release, this library is comprehensive: you can request all
Expand Down
7 changes: 6 additions & 1 deletion lib/twitter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ class << self
#
# @return [Twitter::Client]
def client
Twitter::Client.new(options)
if @client && @client.options == self.options
@client
else
@client = Twitter::Client.new(options)
end
end

# @return [Hash]
def options
@options = {}
Twitter::Configurable.keys.each do |key|
Expand Down
4 changes: 1 addition & 3 deletions lib/twitter/base.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
module Twitter
class Base
attr_reader :attrs
alias body attrs
alias to_hash attrs

# Define methods that retrieve the value from an initialized instance variable Hash, using the attribute as a key
Expand Down Expand Up @@ -74,7 +73,7 @@ def self.fetch_or_new(attrs={})
# @param attrs [Hash]
# @return [Twitter::Base]
def initialize(attrs={})
self.update(attrs)
@attrs = attrs
end

# Fetches an attribute of an object using hash notation
Expand All @@ -91,7 +90,6 @@ def [](method)
# @param attrs [Hash]
# @return [Twitter::Base]
def update(attrs)
@attrs ||= {}
@attrs.update(attrs)
self
end
Expand Down
16 changes: 15 additions & 1 deletion lib/twitter/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require 'twitter/metadata'
require 'twitter/oembed'
require 'twitter/place'
require 'twitter/rate_limit'
require 'twitter/rate_limit_status'
require 'twitter/relationship'
require 'twitter/saved_search'
Expand All @@ -35,6 +36,7 @@ module Twitter
# @see http://dev.twitter.com/pages/every_developer
class Client
include Twitter::Configurable
attr_reader :rate_limit
MAX_USERS_PER_REQUEST = 100
METHOD_RATE_LIMITED = {
:accept => false,
Expand Down Expand Up @@ -173,6 +175,16 @@ def initialize(options={})
Twitter::Configurable.keys.each do |key|
instance_variable_set("@#{key}", options[key] || Twitter.options[key])
end
@rate_limit = Twitter::RateLimit.new
end

# @return [Hash]
def options
@options = {}
Twitter::Configurable.keys.each do |key|
@options[key] = instance_variable_get("@#{key}")
end
@options
end

# Check whether a method is rate limited
Expand Down Expand Up @@ -2876,7 +2888,7 @@ def request(method, path, params, options)
request_headers[:authorization] = authorization.to_s
end
connection.url_prefix = options[:endpoint] || @endpoint
connection.run_request(method.to_sym, path, nil, request_headers) do |request|
response = connection.run_request(method.to_sym, path, nil, request_headers) do |request|
unless params.empty?
case request.method
when :post, :put
Expand All @@ -2887,6 +2899,8 @@ def request(method, path, params, options)
end
yield request if block_given?
end.env
@rate_limit.update(response[:response_headers])
response
rescue Faraday::Error::ClientError
raise Twitter::Error::ClientError
end
Expand Down
14 changes: 2 additions & 12 deletions lib/twitter/cursor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ def self.from_response(response={}, method=:ids, klass=nil)
# Initializes a new Cursor
#
# @param attrs [Hash]
# @return [Twitter::Base]
# @return [Twitter::Cursor]
def initialize(attrs={}, method=:ids, klass=nil)
self.update(attrs)
@attrs = attrs
@collection = Array(attrs[method.to_sym]).map do |item|
if klass
klass.fetch_or_new(item)
Expand Down Expand Up @@ -56,15 +56,5 @@ def last?
end
alias last last?

# Update the attributes of an object
#
# @param attrs [Hash]
# @return [Twitter::Cursor]
def update(attrs)
@attrs ||= {}
@attrs.update(attrs)
self
end

end
end
2 changes: 0 additions & 2 deletions lib/twitter/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
require 'twitter/response/parse_json'
require 'twitter/response/raise_client_error'
require 'twitter/response/raise_server_error'
require 'twitter/response/rate_limit'
require 'twitter/version'

module Twitter
Expand Down Expand Up @@ -58,7 +57,6 @@ def middleware
builder.use Twitter::Response::RaiseClientError # Handle 4xx server responses
builder.use Twitter::Response::ParseJson # Parse JSON response bodies using MultiJson
builder.use Twitter::Response::RaiseServerError # Handle 5xx server responses
builder.use Twitter::Response::RateLimit # Update RateLimit object
builder.adapter Faraday.default_adapter # Set Faraday's HTTP adapter
end
)
Expand Down
5 changes: 3 additions & 2 deletions lib/twitter/error.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Twitter
# Custom error class for rescuing from all Twitter errors
class Error < StandardError
attr_reader :wrapped_exception
attr_reader :rate_limit, :wrapped_exception

def self.errors
@errors ||= Hash[descendants.map{|klass| [klass.const_get(:HTTP_STATUS_CODE), klass]}]
Expand All @@ -15,7 +15,8 @@ def self.descendants
#
# @param exception [Exception, String]
# @return [Twitter::Error]
def initialize(exception=$!)
def initialize(exception=$!, response_headers={})
@rate_limit = Twitter::RateLimit.new(response_headers)
if exception.respond_to?(:backtrace)
super(exception.message)
@wrapped_exception = exception
Expand Down
4 changes: 2 additions & 2 deletions lib/twitter/error/client_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class ClientError < Twitter::Error
#
# @param body [Hash]
# @return [Twitter::Error]
def self.from_response_body(body)
new(parse_error(body))
def self.from_response(response={})
new(parse_error(response[:body]), response[:response_headers])
end

private
Expand Down
12 changes: 10 additions & 2 deletions lib/twitter/error/server_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ class Error
class ServerError < Twitter::Error
MESSAGE = "Server Error"

# Create a new error from an HTTP environment
#
# @param body [Hash]
# @return [Twitter::Error]
def self.from_response(response={})
new(nil, response[:response_headers])
end

# Initializes a new ServerError object
#
# @param message [String]
# @return [Twitter::Error::ServerError]
def initialize(message=nil)
super(message || self.class.const_get(:MESSAGE))
def initialize(message=nil, response_headers={})
super((message || self.class.const_get(:MESSAGE)), response_headers)
end

end
Expand Down
6 changes: 3 additions & 3 deletions lib/twitter/identity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def self.fetch(attrs)
# Stores an object in the identity map.
#
# @param attrs [Hash]
# @return [Twitter::Base]
# @return [Twitter::Identity]
def self.store(object)
Twitter.identity_map[self] ||= {}
object.id && Twitter.identity_map[self][object.id] = object || super(object)
Expand All @@ -29,9 +29,9 @@ def self.store(object)
#
# @param attrs [Hash]
# @raise [ArgumentError] Error raised when supplied argument is missing an :id key.
# @return [Twitter::Base]
# @return [Twitter::Identity]
def initialize(attrs={})
self.update(attrs)
super
raise ArgumentError, "argument must have an :id key" unless self.id
end

Expand Down
40 changes: 19 additions & 21 deletions lib/twitter/rate_limit.rb
Original file line number Diff line number Diff line change
@@ -1,47 +1,45 @@
require 'singleton'

module Twitter
class RateLimit
include Singleton
attr_accessor :response_headers
alias headers response_headers
attr_reader :attrs
alias to_hash attrs

# @deprecated This method exists to provide backwards compatibility to when
# Twitter::RateLimit was a singleton. Safe to remove in version 4.
def self.instance
Twitter.rate_limit
end

def initialize
self.reset!
# @return [Twitter::RateLimit]
def initialize(attrs={})
@attrs = attrs
end

# @return [String]
def class
@response_headers.values_at('x-ratelimit-class', 'X-RateLimit-Class').compact.first
@attrs.values_at('x-ratelimit-class', 'X-RateLimit-Class').compact.first
end

# @return [Integer]
def limit
limit = @response_headers.values_at('x-ratelimit-limit', 'X-RateLimit-Limit').compact.first
limit = @attrs.values_at('x-ratelimit-limit', 'X-RateLimit-Limit').compact.first
limit.to_i if limit
end

# @return [Integer]
def remaining
remaining = @response_headers.values_at('x-ratelimit-remaining', 'X-RateLimit-Remaining').compact.first
remaining = @attrs.values_at('x-ratelimit-remaining', 'X-RateLimit-Remaining').compact.first
remaining.to_i if remaining
end

# @return [Twitter::RateLimit]
def reset!
@response_headers = {}
self
end

# @return [Time]
def reset_at
reset = @response_headers.values_at('x-ratelimit-reset', 'X-RateLimit-Reset').compact.first
reset = @attrs.values_at('x-ratelimit-reset', 'X-RateLimit-Reset').compact.first
Time.at(reset.to_i) if reset
end

# @return [Integer]
def reset_in
if retry_after = @response_headers.values_at('retry-after', 'Retry-After').compact.first
if retry_after = @attrs.values_at('retry-after', 'Retry-After').compact.first
retry_after.to_i
elsif reset_at
[(reset_at - Time.now).ceil, 0].max
Expand All @@ -51,10 +49,10 @@ def reset_in

# Update the attributes of a Relationship
#
# @param response_headers [Hash]
# @param attrs [Hash]
# @return [Twitter::RateLimit]
def update(response_headers)
@response_headers.update(response_headers)
def update(attrs)
@attrs.update(attrs)
self
end

Expand Down
9 changes: 8 additions & 1 deletion lib/twitter/relationship.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
module Twitter
class Relationship < Twitter::Base

# Initializes a new object
#
# @param attrs [Hash]
# @return [Twitter::Relationship]
def initialize(attrs={})
@attrs = attrs[:relationship]
end

# @return [Twitter::User]
def source
@source ||= Twitter::SourceUser.fetch_or_new(@attrs[:source]) unless @attrs[:source].nil?
Expand All @@ -20,7 +28,6 @@ def target
# @param attrs [Hash]
# @return [Twitter::Relationship]
def update(attrs)
@attrs ||= {}
@attrs.update(attrs[:relationship]) unless attrs[:relationship].nil?
self
end
Expand Down
2 changes: 1 addition & 1 deletion lib/twitter/response/raise_client_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class RaiseClientError < Faraday::Response::Middleware
def on_complete(env)
status_code = env[:status].to_i
error_class = Twitter::Error::ClientError.errors[status_code]
raise error_class.from_response_body(env[:body]) if error_class
raise error_class.from_response(env) if error_class
end

end
Expand Down
2 changes: 1 addition & 1 deletion lib/twitter/response/raise_server_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class RaiseServerError < Faraday::Response::Middleware
def on_complete(env)
status_code = env[:status].to_i
error_class = Twitter::Error::ServerError.errors[status_code]
raise error_class.new if error_class
raise error_class.from_response(env) if error_class
end

end
Expand Down
14 changes: 0 additions & 14 deletions lib/twitter/response/rate_limit.rb

This file was deleted.

8 changes: 8 additions & 0 deletions spec/twitter/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@
client2.verify_credentials.screen_name.should eq 'pengwynn'
end

describe "#initalize" do
it "returns a different rate limit object for a new client" do
client1 = Twitter::Client.new
client2 = Twitter::Client.new
client1.rate_limit.should_not eq client2.rate_limit
end
end

describe "#rate_limited?" do
before do
@client = Twitter::Client.new
Expand Down
Loading

0 comments on commit 6e9da0d

Please sign in to comment.