Merge branch 'dougforpres-auth-retry'

This commit is contained in:
Steven Bazyl 2014-12-17 12:40:15 -08:00
commit 0b4b24d06d
4 changed files with 121 additions and 25 deletions

View File

@ -123,6 +123,8 @@ in the credentials. Detailed instructions on how to enable delegation for your d
The API client can automatically retry requests for recoverable errors. To enable retries, set the `client.retries` property to The API client can automatically retry requests for recoverable errors. To enable retries, set the `client.retries` property to
the number of additional attempts. To avoid flooding servers, retries invovle a 1 second delay that increases on each subsequent retry. the number of additional attempts. To avoid flooding servers, retries invovle a 1 second delay that increases on each subsequent retry.
In the case of authentication token expiry, the API client will attempt to refresh the token and retry the failed operation - this
is a specific exception to the retry rules.
The default value for retries is 0, but will be enabled by default in future releases. The default value for retries is 0, but will be enabled by default in future releases.

View File

@ -118,6 +118,7 @@ module Google
self.key = options[:key] self.key = options[:key]
self.user_ip = options[:user_ip] self.user_ip = options[:user_ip]
self.retries = options.fetch(:retries) { 0 } self.retries = options.fetch(:retries) { 0 }
self.expired_auth_retry = options.fetch(:expired_auth_retry) { true }
@discovery_uris = {} @discovery_uris = {}
@discovery_documents = {} @discovery_documents = {}
@discovered_apis = {} @discovered_apis = {}
@ -255,6 +256,13 @@ module Google
# Number of retries # Number of retries
attr_accessor :retries attr_accessor :retries
##
# Whether or not an expired auth token should be re-acquired
# (and the operation retried) regardless of retries setting
# @return [Boolean]
# Auto retry on auth expiry
attr_accessor :expired_auth_retry
## ##
# Returns the URI for the directory document. # Returns the URI for the directory document.
# #
@ -613,28 +621,40 @@ module Google
request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
tries = 1 + (options[:retries] || self.retries) tries = 1 + (options[:retries] || self.retries)
attempt = 0
Retriable.retriable :tries => tries, Retriable.retriable :tries => tries,
:on => [TransmissionError], :on => [TransmissionError],
:on_retry => client_error_handler(request.authorization), :on_retry => client_error_handler,
:interval => lambda {|attempts| (2 ** attempts) + rand} do :interval => lambda {|attempts| (2 ** attempts) + rand} do
result = request.send(connection, true) attempt += 1
case result.status # This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
when 200...300 # auth to be re-attempted without having to retry all sorts of other failures like
result # NotFound, etc
when 301, 302, 303, 307 Retriable.retriable :tries => ((expired_auth_retry || tries > 1) && attempt == 1) ? 2 : 1,
request = generate_request(request.to_hash.merge({ :on => [AuthorizationError],
:uri => result.headers['location'], :on_retry => authorization_error_handler(request.authorization) do
:api_method => nil result = request.send(connection, true)
}))
raise RedirectError.new(result.headers['location'], result) case result.status
when 400...500 when 200...300
raise ClientError.new(result.error_message || "A client error has occurred", result) result
when 500...600 when 301, 302, 303, 307
raise ServerError.new(result.error_message || "A server error has occurred", result) request = generate_request(request.to_hash.merge({
else :uri => result.headers['location'],
raise TransmissionError.new(result.error_message || "A transmission error has occurred", result) :api_method => nil
}))
raise RedirectError.new(result.headers['location'], result)
when 401
raise AuthorizationError.new(result.error_message || 'Invalid/Expired Authentication', result)
when 400, 402...500
raise ClientError.new(result.error_message || "A client error has occurred", result)
when 500...600
raise ServerError.new(result.error_message || "A server error has occurred", result)
else
raise TransmissionError.new(result.error_message || "A transmission error has occurred", result)
end
end end
end end
end end
@ -682,18 +702,17 @@ module Google
## ##
# Returns on proc for special processing of retries as not all client errors # Returns on proc for special processing of retries for authorization errors
# are recoverable. Only 401s should be retried and only if the credentials # Only 401s should be retried and only if the credentials are refreshable
# are refreshable
# #
# @param [#fetch_access_token!] authorization # @param [#fetch_access_token!] authorization
# OAuth 2 credentials # OAuth 2 credentials
# @return [Proc] # @return [Proc]
def client_error_handler(authorization) def authorization_error_handler(authorization)
can_refresh = authorization.respond_to?(:refresh_token) && auto_refresh_token can_refresh = authorization.respond_to?(:refresh_token) && auto_refresh_token
Proc.new do |exception, tries| Proc.new do |exception, tries|
next unless exception.kind_of?(ClientError) next unless exception.kind_of?(AuthorizationError)
if exception.result.status == 401 && can_refresh && tries == 1 if can_refresh
begin begin
logger.debug("Attempting refresh of access token & retry of request") logger.debug("Attempting refresh of access token & retry of request")
authorization.fetch_access_token! authorization.fetch_access_token!
@ -705,6 +724,17 @@ module Google
end end
end end
##
# Returns on proc for special processing of retries as not all client errors
# are recoverable. Only 401s should be retried (via authorization_error_handler)
#
# @return [Proc]
def client_error_handler
Proc.new do |exception, tries|
raise exception if exception.kind_of?(ClientError)
end
end
end end
end end

View File

@ -43,6 +43,11 @@ module Google
class ClientError < TransmissionError class ClientError < TransmissionError
end end
##
# A 401 HTTP error occurred.
class AuthorizationError < ClientError
end
## ##
# A 5xx class HTTP error occurred. # A 5xx class HTTP error occurred.
class ServerError < TransmissionError class ServerError < TransmissionError

View File

@ -200,7 +200,7 @@ RSpec.describe Google::APIClient do
) )
end end
it 'should refresh tokens on 401 tokens' do it 'should refresh tokens on 401 errors' do
client.authorization.access_token = '12345' client.authorization.access_token = '12345'
expect(client.authorization).to receive(:fetch_access_token!) expect(client.authorization).to receive(:fetch_access_token!)
@ -290,4 +290,63 @@ RSpec.describe Google::APIClient do
end end
end end
describe 'when retries disabled and expired_auth_retry on (default)' do
before do
client.retries = 0
end
after do
@connection.verify
end
it 'should refresh tokens on 401 errors' do
client.authorization.access_token = '12345'
expect(client.authorization).to receive(:fetch_access_token!)
@connection = stub_connection do |stub|
stub.get('/foo') do |env|
[401, {}, '{}']
end
stub.get('/foo') do |env|
[200, {}, '{}']
end
end
client.execute(
:uri => 'https://www.gogole.com/foo',
:connection => @connection
)
end
end
describe 'when retries disabled and expired_auth_retry off' do
before do
client.retries = 0
client.expired_auth_retry = false
end
it 'should not refresh tokens on 401 errors' do
client.authorization.access_token = '12345'
expect(client.authorization).not_to receive(:fetch_access_token!)
@connection = stub_connection do |stub|
stub.get('/foo') do |env|
[401, {}, '{}']
end
stub.get('/foo') do |env|
[200, {}, '{}']
end
end
resp = client.execute(
:uri => 'https://www.gogole.com/foo',
:connection => @connection
)
expect(resp.response.status).to be == 401
end
end
end end