From ad93a6ab60524b6e563ad64c4c3a386c0c5a3127 Mon Sep 17 00:00:00 2001 From: Tim Morgan Date: Sun, 28 Feb 2021 09:46:52 -0600 Subject: [PATCH] Automatically handle rate limiting errors We will sleep the prescribed number of seconds, then retry the request. To disable, you can set retry_when_rate_limited to false. --- README.md | 11 +++++++++ lib/pco/api.rb | 7 +++--- lib/pco/api/endpoint.rb | 23 ++++++++++++++++++- spec/pco/api/endpoint_spec.rb | 42 +++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 14a08a8..3c598d2 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,17 @@ In the case of validation errors, the `message` is a summary string built from t Alternatively, you may rescue `PCO::API::Errors::BaseError` and branch your code based on the status code returned by calling `error.status`. +### TooManyRequests Error + +By default, PCO::API::Endpoint will sleep and retry a request that fails with TooManyRequests due +to rate limiting. If you would rather catch and handle such errors yourself, you can disable this +behavior like this: + +```ruby +api = PCO::API.new(...) +api.retry_when_rate_limited = false +``` + ## Copyright & License Copyright Ministry Centered Technologies. Licensed MIT. diff --git a/lib/pco/api.rb b/lib/pco/api.rb index 962bc51..79d3c54 100644 --- a/lib/pco/api.rb +++ b/lib/pco/api.rb @@ -3,9 +3,10 @@ module PCO module API - module_function - def new(**args) - Endpoint.new(**args) + class << self + def new(**args) + Endpoint.new(**args) + end end end end diff --git a/lib/pco/api/endpoint.rb b/lib/pco/api/endpoint.rb index 86053d9..8415825 100644 --- a/lib/pco/api/endpoint.rb +++ b/lib/pco/api/endpoint.rb @@ -12,6 +12,8 @@ class Response < Hash class Endpoint attr_reader :url, :last_result + attr_accessor :retry_when_rate_limited + def initialize(url: URL, oauth_access_token: nil, basic_auth_token: nil, basic_auth_secret: nil, connection: nil) @url = url @oauth_access_token = oauth_access_token @@ -19,6 +21,7 @@ def initialize(url: URL, oauth_access_token: nil, basic_auth_token: nil, basic_a @basic_auth_secret = basic_auth_secret @connection = connection || _build_connection @cache = {} + @retry_when_rate_limited = true end def method_missing(method_name, *_args) @@ -29,7 +32,7 @@ def [](id) _build_endpoint(id.to_s) end - def respond_to?(method_name) + def respond_to?(method_name, _include_all = false) endpoint = _build_endpoint(method_name.to_s) begin endpoint.get @@ -43,6 +46,8 @@ def respond_to?(method_name) def get(params = {}) @last_result = @connection.get(@url, params) _build_response(@last_result) + rescue Errors::TooManyRequests => e + _retry_after_timeout?(e) ? retry : raise end def post(body = {}) @@ -50,6 +55,8 @@ def post(body = {}) req.body = _build_body(body) end _build_response(@last_result) + rescue Errors::TooManyRequests => e + _retry_after_timeout?(e) ? retry : raise end def patch(body = {}) @@ -57,6 +64,8 @@ def patch(body = {}) req.body = _build_body(body) end _build_response(@last_result) + rescue Errors::TooManyRequests => e + _retry_after_timeout?(e) ? retry : raise end def delete @@ -66,6 +75,8 @@ def delete else _build_response(@last_result) end + rescue Errors::TooManyRequests => e + _retry_after_timeout?(e) ? retry : raise end private @@ -135,6 +146,16 @@ def _build_connection faraday.adapter :excon end end + + def _retry_after_timeout?(e) + if @retry_when_rate_limited + secs = e.headers['Retry-After'] + Kernel.sleep(secs ? secs.to_i : 1) + true + else + false + end + end end end end diff --git a/spec/pco/api/endpoint_spec.rb b/spec/pco/api/endpoint_spec.rb index 7bc9ad7..0a77df4 100644 --- a/spec/pco/api/endpoint_spec.rb +++ b/spec/pco/api/endpoint_spec.rb @@ -124,6 +124,48 @@ }.to raise_error(PCO::API::Errors::ServerError) end end + + context 'given a 429 error due to rate limiting' do + subject { base.people.v2 } + + let(:result) do + { + 'type' => 'Organization', + 'id' => '1', + 'name' => 'Ministry Centered Technologies', + 'links' => {} + } + end + + before do + stub_request(:get, 'https://api.planningcenteronline.com/people/v2') + .to_return([ + { status: 429, headers: { 'retry-after' => '2' } }, + { status: 200, body: { data: result }.to_json, headers: { 'Content-Type' => 'application/vnd.api+json' } } + ]) + end + + context 'given retry_when_rate_limited is true' do + before do + subject.retry_when_rate_limited = true + end + + it 'sleeps, then makes the call again' do + expect(Kernel).to receive(:sleep).with(2) + expect(subject.get).to be_a(Hash) + end + end + + context 'given retry_when_rate_limited is false' do + before do + subject.retry_when_rate_limited = false + end + + it 'raises the TooManyRequests error' do + expect { subject.get }.to raise_error(PCO::API::Errors::TooManyRequests) + end + end + end end describe '#post' do