Skip to content

feat: add ratelimit support #68

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

Merged
merged 8 commits into from
Feb 21, 2020
Merged
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
9 changes: 9 additions & 0 deletions examples/example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,12 @@
response = client.api_keys._(api_key_id).delete
puts response.status_code
puts response.headers

# Rate limit information
response = client.version('v3').api_keys._(api_key_id).get
puts response.ratelimit.limit
puts response.ratelimit.remaining
puts response.ratelimit.reset
puts response.ratelimit.exceeded?
# Sleep the current thread until the reset has happened
response.ratelimit.wait!
57 changes: 57 additions & 0 deletions lib/ruby_http_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,49 @@ module SendGrid

# Holds the response from an API call.
class Response
# Provide useful functionality around API rate limiting.
class Ratelimit
attr_reader :limit, :remaining, :reset

# * *Args* :
# - +limit+ -> The total number of requests allowed within a rate limit window
# - +remaining+ -> The number of requests that have been processed within this current rate limit window
# - +reset+ -> The time (in seconds since Unix Epoch) when the rate limit will reset
def initialize(limit, remaining, reset)
@limit = limit.to_i
@remaining = remaining.to_i
@reset = Time.at reset.to_i
end

def exceeded?
remaining <= 0
end

# * *Returns* :
# - The number of requests that have been used out of this
# rate limit window
def used
limit - remaining
end

# Sleep until the reset time arrives. If given a block, it will
# be called after sleeping is finished.
#
# * *Returns* :
# - The amount of time (in seconds) that the rate limit slept
# for.
def wait!
now = Time.now.utc.to_i
duration = (reset.to_i - now) + 1

sleep duration if duration >= 0

yield if block_given?

duration
end
end

# * *Args* :
# - +response+ -> A NET::HTTP response object
#
Expand All @@ -21,6 +64,20 @@ def initialize(response)
def parsed_body
@parsed_body ||= JSON.parse(@body, symbolize_names: true)
end

def ratelimit
return @ratelimit unless @ratelimit.nil?

limit = headers['X-RateLimit-Limit']
remaining = headers['X-RateLimit-Remaining']
reset = headers['X-RateLimit-Reset']

# Guard against possibility that one (or probably, all) of the
# needed headers were not returned.
@ratelimit = Ratelimit.new(limit, remaining, reset) if limit && remaining && reset

@ratelimit
end
end

# A simple REST client.
Expand Down
40 changes: 40 additions & 0 deletions test/test_ruby_http_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ def initialize(response)
end
end

class MockHttpResponse
attr_reader :code, :body, :headers

def initialize(code, body, headers)
@code = code
@body = body
@headers = headers
end

alias to_hash headers
end

class MockResponseWithRequestBody < MockResponse
attr_reader :request_body

Expand Down Expand Up @@ -232,6 +244,34 @@ def test__
assert_equal(['test'], url1.url_path)
end

def test_ratelimit_core
expiry = Time.now.to_i + 1
rl = SendGrid::Response::Ratelimit.new(500, 100, expiry)
rl2 = SendGrid::Response::Ratelimit.new(500, 0, expiry)

refute rl.exceeded?
assert rl2.exceeded?

assert_equal(rl.used, 400)
assert_equal(rl2.used, 500)
end

def test_response_ratelimit_parsing
headers = {
'X-RateLimit-Limit' => '500',
'X-RateLimit-Remaining' => '300',
'X-RateLimit-Reset' => Time.now.to_i.to_s
}

body = ''
code = 204
http_response = MockHttpResponse.new(code, body, headers)
response = SendGrid::Response.new(http_response)

refute_nil response.ratelimit
refute response.ratelimit.exceeded?
end

def test_method_missing
response = @client.get
assert_equal(200, response.status_code)
Expand Down