Skip to content

Commit

Permalink
Merge pull request #19 from planningcenter/rate-limit-retry
Browse files Browse the repository at this point in the history
feat(RateLimiting): automatically retry requests when rate limited
  • Loading branch information
seven1m authored Mar 1, 2021
2 parents cd0a0fc + ad93a6a commit 3516c8e
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 4 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 4 additions & 3 deletions lib/pco/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 22 additions & 1 deletion lib/pco/api/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ 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
@basic_auth_token = basic_auth_token
@basic_auth_secret = basic_auth_secret
@connection = connection || _build_connection
@cache = {}
@retry_when_rate_limited = true
end

def method_missing(method_name, *_args)
Expand All @@ -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
Expand All @@ -43,20 +46,26 @@ 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 = {})
@last_result = @connection.post(@url) do |req|
req.body = _build_body(body)
end
_build_response(@last_result)
rescue Errors::TooManyRequests => e
_retry_after_timeout?(e) ? retry : raise
end

def patch(body = {})
@last_result = @connection.patch(@url) do |req|
req.body = _build_body(body)
end
_build_response(@last_result)
rescue Errors::TooManyRequests => e
_retry_after_timeout?(e) ? retry : raise
end

def delete
Expand All @@ -66,6 +75,8 @@ def delete
else
_build_response(@last_result)
end
rescue Errors::TooManyRequests => e
_retry_after_timeout?(e) ? retry : raise
end

private
Expand Down Expand Up @@ -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
42 changes: 42 additions & 0 deletions spec/pco/api/endpoint_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3516c8e

Please sign in to comment.