Skip to content

Commit 248b8cd

Browse files
authored
feat: add ratelimit support (#68)
* Simple core class for dealing with rate limit info. * Add a SendGrid::Response#ratelimit property that exposes a SendGrid::Response::Ratelimit instance for that specific response. Add appropriate docs. * Clean up style linting offenses. * Remove redundant `@reset` assignment. Thanks, @cseeman, nice catch!
1 parent 127083f commit 248b8cd

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

examples/example.rb

+9
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,12 @@
8888
response = client.api_keys._(api_key_id).delete
8989
puts response.status_code
9090
puts response.headers
91+
92+
# Rate limit information
93+
response = client.version('v3').api_keys._(api_key_id).get
94+
puts response.ratelimit.limit
95+
puts response.ratelimit.remaining
96+
puts response.ratelimit.reset
97+
puts response.ratelimit.exceeded?
98+
# Sleep the current thread until the reset has happened
99+
response.ratelimit.wait!

lib/ruby_http_client.rb

+57
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,49 @@ module SendGrid
66

77
# Holds the response from an API call.
88
class Response
9+
# Provide useful functionality around API rate limiting.
10+
class Ratelimit
11+
attr_reader :limit, :remaining, :reset
12+
13+
# * *Args* :
14+
# - +limit+ -> The total number of requests allowed within a rate limit window
15+
# - +remaining+ -> The number of requests that have been processed within this current rate limit window
16+
# - +reset+ -> The time (in seconds since Unix Epoch) when the rate limit will reset
17+
def initialize(limit, remaining, reset)
18+
@limit = limit.to_i
19+
@remaining = remaining.to_i
20+
@reset = Time.at reset.to_i
21+
end
22+
23+
def exceeded?
24+
remaining <= 0
25+
end
26+
27+
# * *Returns* :
28+
# - The number of requests that have been used out of this
29+
# rate limit window
30+
def used
31+
limit - remaining
32+
end
33+
34+
# Sleep until the reset time arrives. If given a block, it will
35+
# be called after sleeping is finished.
36+
#
37+
# * *Returns* :
38+
# - The amount of time (in seconds) that the rate limit slept
39+
# for.
40+
def wait!
41+
now = Time.now.utc.to_i
42+
duration = (reset.to_i - now) + 1
43+
44+
sleep duration if duration >= 0
45+
46+
yield if block_given?
47+
48+
duration
49+
end
50+
end
51+
952
# * *Args* :
1053
# - +response+ -> A NET::HTTP response object
1154
#
@@ -21,6 +64,20 @@ def initialize(response)
2164
def parsed_body
2265
@parsed_body ||= JSON.parse(@body, symbolize_names: true)
2366
end
67+
68+
def ratelimit
69+
return @ratelimit unless @ratelimit.nil?
70+
71+
limit = headers['X-RateLimit-Limit']
72+
remaining = headers['X-RateLimit-Remaining']
73+
reset = headers['X-RateLimit-Reset']
74+
75+
# Guard against possibility that one (or probably, all) of the
76+
# needed headers were not returned.
77+
@ratelimit = Ratelimit.new(limit, remaining, reset) if limit && remaining && reset
78+
79+
@ratelimit
80+
end
2481
end
2582

2683
# A simple REST client.

test/test_ruby_http_client.rb

+40
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ def initialize(response)
1212
end
1313
end
1414

15+
class MockHttpResponse
16+
attr_reader :code, :body, :headers
17+
18+
def initialize(code, body, headers)
19+
@code = code
20+
@body = body
21+
@headers = headers
22+
end
23+
24+
alias to_hash headers
25+
end
26+
1527
class MockResponseWithRequestBody < MockResponse
1628
attr_reader :request_body
1729

@@ -232,6 +244,34 @@ def test__
232244
assert_equal(['test'], url1.url_path)
233245
end
234246

247+
def test_ratelimit_core
248+
expiry = Time.now.to_i + 1
249+
rl = SendGrid::Response::Ratelimit.new(500, 100, expiry)
250+
rl2 = SendGrid::Response::Ratelimit.new(500, 0, expiry)
251+
252+
refute rl.exceeded?
253+
assert rl2.exceeded?
254+
255+
assert_equal(rl.used, 400)
256+
assert_equal(rl2.used, 500)
257+
end
258+
259+
def test_response_ratelimit_parsing
260+
headers = {
261+
'X-RateLimit-Limit' => '500',
262+
'X-RateLimit-Remaining' => '300',
263+
'X-RateLimit-Reset' => Time.now.to_i.to_s
264+
}
265+
266+
body = ''
267+
code = 204
268+
http_response = MockHttpResponse.new(code, body, headers)
269+
response = SendGrid::Response.new(http_response)
270+
271+
refute_nil response.ratelimit
272+
refute response.ratelimit.exceeded?
273+
end
274+
235275
def test_method_missing
236276
response = @client.get
237277
assert_equal(200, response.status_code)

0 commit comments

Comments
 (0)