From b00b3be4a9b5085bf0b3d8a5667aa96feaa47bfc Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Sat, 4 Nov 2017 14:16:18 -0600 Subject: [PATCH 1/6] Simple core class for dealing with rate limit info. --- lib/ruby_http_client.rb | 46 +++++++++++++++++++++++++++++++++++ test/test_ruby_http_client.rb | 12 +++++++++ 2 files changed, 58 insertions(+) diff --git a/lib/ruby_http_client.rb b/lib/ruby_http_client.rb index 4b9d249..31d2086 100644 --- a/lib/ruby_http_client.rb +++ b/lib/ruby_http_client.rb @@ -6,6 +6,52 @@ 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 = reset.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 + + if duration >= 0 + sleep duration + end + + yield if block_given? + + duration + end + end + # * *Args* : # - +response+ -> A NET::HTTP response object # diff --git a/test/test_ruby_http_client.rb b/test/test_ruby_http_client.rb index e8e2c56..70a2aa6 100644 --- a/test/test_ruby_http_client.rb +++ b/test/test_ruby_http_client.rb @@ -165,6 +165,18 @@ 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_method_missing response = @client.get assert_equal(200, response.status_code) From 30fea9ff8e1dd427c88a3dcabf630d3b87c9d3be Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Sat, 4 Nov 2017 14:37:28 -0600 Subject: [PATCH 2/6] Add a SendGrid::Response#ratelimit property that exposes a SendGrid::Response::Ratelimit instance for that specific response. Add appropriate docs. --- examples/example.rb | 9 +++++++++ lib/ruby_http_client.rb | 16 ++++++++++++++++ test/test_ruby_http_client.rb | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/examples/example.rb b/examples/example.rb index 1832d06..14847ff 100644 --- a/examples/example.rb +++ b/examples/example.rb @@ -69,3 +69,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! diff --git a/lib/ruby_http_client.rb b/lib/ruby_http_client.rb index 31d2086..50f2004 100644 --- a/lib/ruby_http_client.rb +++ b/lib/ruby_http_client.rb @@ -67,6 +67,22 @@ 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. + if limit && remaining && reset + @ratelimit = Ratelimit.new(limit, remaining, reset) + end + + @ratelimit + end end # A simple REST client. diff --git a/test/test_ruby_http_client.rb b/test/test_ruby_http_client.rb index 70a2aa6..d9e7de3 100644 --- a/test/test_ruby_http_client.rb +++ b/test/test_ruby_http_client.rb @@ -11,6 +11,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 MockRequest < SendGrid::Client def make_request(_http, _request) response = {} @@ -177,6 +189,23 @@ def test_ratelimit_core 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}", + } + + 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) From 2f0c5cbdaca2762c4397f4f35cb5bc610f106bd1 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Fri, 19 Oct 2018 20:41:06 -0600 Subject: [PATCH 3/6] Clean up style linting offenses. --- lib/ruby_http_client.rb | 6 ++---- test/test_ruby_http_client.rb | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/ruby_http_client.rb b/lib/ruby_http_client.rb index 9631e8a..3cdbcbe 100644 --- a/lib/ruby_http_client.rb +++ b/lib/ruby_http_client.rb @@ -42,16 +42,14 @@ def wait! now = Time.now.utc.to_i duration = (reset.to_i - now) + 1 - if duration >= 0 - sleep duration - end + sleep duration if duration >= 0 yield if block_given? duration end end - + # * *Args* : # - +response+ -> A NET::HTTP response object # diff --git a/test/test_ruby_http_client.rb b/test/test_ruby_http_client.rb index 6f87e15..2a431da 100644 --- a/test/test_ruby_http_client.rb +++ b/test/test_ruby_http_client.rb @@ -21,7 +21,7 @@ def initialize(code, body, headers) @headers = headers end - alias :to_hash :headers + alias to_hash headers end class MockRequest < SendGrid::Client @@ -185,7 +185,7 @@ def test__ def test_ratelimit_core expiry = Time.now.to_i + 1 - rl = SendGrid::Response::Ratelimit.new(500, 100, expiry) + rl = SendGrid::Response::Ratelimit.new(500, 100, expiry) rl2 = SendGrid::Response::Ratelimit.new(500, 0, expiry) refute rl.exceeded? @@ -196,11 +196,10 @@ def test_ratelimit_core end def test_response_ratelimit_parsing - headers = { 'X-RateLimit-Limit' => '500', 'X-RateLimit-Remaining' => '300', - 'X-RateLimit-Reset' => "#{Time.now.to_i}", + 'X-RateLimit-Reset' => Time.now.to_i.to_s } body = '' From 52bf4a48b3fbd1dcff3893d48e117b7c4b67e706 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Mon, 11 Nov 2019 09:39:47 -0700 Subject: [PATCH 4/6] Remove redundant `@reset` assignment. Thanks, @cseeman, nice catch! --- lib/ruby_http_client.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ruby_http_client.rb b/lib/ruby_http_client.rb index 3cdbcbe..6df0f5c 100644 --- a/lib/ruby_http_client.rb +++ b/lib/ruby_http_client.rb @@ -17,7 +17,6 @@ class Ratelimit def initialize(limit, remaining, reset) @limit = limit.to_i @remaining = remaining.to_i - @reset = reset.to_i @reset = Time.at reset.to_i end From 5fead7d3618d4c7049c418958bdbdffdaeb2a130 Mon Sep 17 00:00:00 2001 From: childish-sambino Date: Fri, 21 Feb 2020 12:38:42 -0600 Subject: [PATCH 5/6] Update test_ruby_http_client.rb --- test/test_ruby_http_client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_ruby_http_client.rb b/test/test_ruby_http_client.rb index 8f9c1c5..6ae8f46 100644 --- a/test/test_ruby_http_client.rb +++ b/test/test_ruby_http_client.rb @@ -22,7 +22,7 @@ def initialize(code, body, headers) end alias to_hash headers -end +end class MockResponseWithRequestBody < MockResponse attr_reader :request_body From bcb7462cafdef491543a21a2d1407c353f4bd4c0 Mon Sep 17 00:00:00 2001 From: childish-sambino Date: Fri, 21 Feb 2020 12:40:06 -0600 Subject: [PATCH 6/6] Update ruby_http_client.rb --- lib/ruby_http_client.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/ruby_http_client.rb b/lib/ruby_http_client.rb index 903dffa..f7fe5cb 100644 --- a/lib/ruby_http_client.rb +++ b/lib/ruby_http_client.rb @@ -74,9 +74,7 @@ def ratelimit # Guard against possibility that one (or probably, all) of the # needed headers were not returned. - if limit && remaining && reset - @ratelimit = Ratelimit.new(limit, remaining, reset) - end + @ratelimit = Ratelimit.new(limit, remaining, reset) if limit && remaining && reset @ratelimit end