-
Notifications
You must be signed in to change notification settings - Fork 1.5k
EC2MetadataCredentials and ECSCredentials retry #1114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
ec1e757
44fb727
bb6745d
90b410b
ea30bd7
1917d31
39705b1
783a1ab
0c52b9f
e25adce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,23 +9,30 @@ require('../metadata_service'); | |
* can connect, and credentials are available, these will be used with zero | ||
* configuration. | ||
* | ||
* This credentials class will timeout after 1 second of inactivity by default. | ||
* This credentials class will by default timeout after 1 second of inactivity | ||
* and retry 3 times. | ||
* If your requests to the EC2 metadata service are timing out, you can increase | ||
* the value by configuring them directly: | ||
* these values by configuring them directly: | ||
* | ||
* ```javascript | ||
* AWS.config.credentials = new AWS.EC2MetadataCredentials({ | ||
* httpOptions: { timeout: 5000 } // 5 second timeout | ||
* httpOptions: { timeout: 5000 }, // 5 second timeout | ||
* maxRetries: 10, // retry 10 times | ||
* retryDelayOptions: { base: 30 } // see AWS.Config for information | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know it's an example, but we should probably show increasing the base retry delay above 100 (current default). That puts it more in line with the comment that says you can increase these values on line 14. |
||
* }); | ||
* ``` | ||
* | ||
* @see AWS.Config.retryDelayOptions | ||
* | ||
* @!macro nobrowser | ||
*/ | ||
AWS.EC2MetadataCredentials = AWS.util.inherit(AWS.Credentials, { | ||
constructor: function EC2MetadataCredentials(options) { | ||
AWS.Credentials.call(this); | ||
|
||
options = options ? AWS.util.copy(options) : {}; | ||
options = AWS.util.merge( | ||
{maxRetries: this.defaultMaxRetries}, options); | ||
if (!options.httpOptions) options.httpOptions = {}; | ||
options.httpOptions = AWS.util.merge( | ||
{timeout: this.defaultTimeout}, options.httpOptions); | ||
|
@@ -39,6 +46,11 @@ AWS.EC2MetadataCredentials = AWS.util.inherit(AWS.Credentials, { | |
*/ | ||
defaultTimeout: 1000, | ||
|
||
/** | ||
* @api private | ||
*/ | ||
defaultMaxRetries: 3, | ||
|
||
/** | ||
* Loads the credentials from the instance metadata service | ||
* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,16 +8,21 @@ var AWS = require('../core'); | |
* in the container. If valid credentials are returned in the response, these | ||
* will be used with zero configuration. | ||
* | ||
* This credentials class will timeout after 1 second of inactivity by default. | ||
* This credentials class will by default timeout after 1 second of inactivity | ||
* and retry 3 times. | ||
* If your requests to the relative URI are timing out, you can increase | ||
* the value by configuring them directly: | ||
* | ||
* ```javascript | ||
* AWS.config.credentials = new AWS.ECSCredentials({ | ||
* httpOptions: { timeout: 5000 } // 5 second timeout | ||
* httpOptions: { timeout: 5000 }, // 5 second timeout | ||
* maxRetries: 10, // retry 10 times | ||
* retryDelayOptions: { base: 30 } // see AWS.Config for information | ||
* }); | ||
* ``` | ||
* | ||
* @see AWS.Config.retryDelayOptions | ||
* | ||
* @!macro nobrowser | ||
*/ | ||
AWS.ECSCredentials = AWS.util.inherit(AWS.Credentials, { | ||
|
@@ -40,6 +45,11 @@ AWS.ECSCredentials = AWS.util.inherit(AWS.Credentials, { | |
*/ | ||
host: '169.254.170.2', | ||
|
||
/** | ||
* @api private | ||
*/ | ||
maxRetries: 3, | ||
|
||
/** | ||
* Sets the name of the ECS environment variable to check for relative URI | ||
* If changed, please change the name in the documentation for defaultProvider | ||
|
@@ -69,22 +79,17 @@ AWS.ECSCredentials = AWS.util.inherit(AWS.Credentials, { | |
*/ | ||
request: function request(path, callback) { | ||
path = path || '/'; | ||
|
||
var data = ''; | ||
var http = AWS.HttpClient.getInstance(); | ||
var httpRequest = new AWS.HttpRequest('http://' + this.host + path); | ||
httpRequest.method = 'GET'; | ||
httpRequest.headers.Accept = 'application/json'; | ||
var httpOptions = this.httpOptions; | ||
|
||
process.nextTick(function() { | ||
http.handleRequest(httpRequest, httpOptions, function(httpResponse) { | ||
httpResponse.on('data', function(chunk) { data += chunk.toString(); }); | ||
httpResponse.on('end', function() { callback(null, data); }); | ||
}, callback); | ||
}); | ||
AWS.util.handleRequestWithTimeoutRetries(httpRequest, this, callback); | ||
}, | ||
|
||
/** | ||
* @api private | ||
*/ | ||
refreshQueue: [], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the refresh queue only implemented for ECS credentials and not EC2 credentials? EC2 credentials has the same issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. EC2MetdataCredentials already had this feature, through the MetadataService. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, so EC2 already supported this, excellent. |
||
|
||
/** | ||
* Loads the credentials from the relative URI specified by container | ||
* | ||
|
@@ -98,18 +103,33 @@ AWS.ECSCredentials = AWS.util.inherit(AWS.Credentials, { | |
*/ | ||
refresh: function refresh(callback) { | ||
var self = this; | ||
var refreshQueue = self.refreshQueue; | ||
if (!callback) callback = function(err) { if (err) throw err; }; | ||
refreshQueue.push({ | ||
provider: self, | ||
errCallback: callback | ||
}); | ||
if (refreshQueue.length > 1) { return; } | ||
|
||
function callbacks(err, creds) { | ||
var call, cb; | ||
while ((call = refreshQueue.shift()) !== undefined) { | ||
cb = call.errCallback; | ||
if (!err) AWS.util.update(call.provider, creds); | ||
cb(err); | ||
} | ||
} | ||
|
||
if (process === undefined) { | ||
callback(AWS.util.error( | ||
callbacks(AWS.util.error( | ||
new Error('No process info available'), | ||
{ code: 'ECSCredentialsProviderFailure' } | ||
)); | ||
return; | ||
} | ||
var relativeUri = this.getECSRelativeUri(); | ||
if (relativeUri === undefined) { | ||
callback(AWS.util.error( | ||
callbacks(AWS.util.error( | ||
new Error('Variable ' + this.environmentVar + ' not set.'), | ||
{ code: 'ECSCredentialsProviderFailure' } | ||
)); | ||
|
@@ -119,13 +139,15 @@ AWS.ECSCredentials = AWS.util.inherit(AWS.Credentials, { | |
this.request(relativeUri, function(err, data) { | ||
if (!err) { | ||
try { | ||
var creds = JSON.parse(data); | ||
if (self.credsFormatIsValid(creds)) { | ||
self.expired = false; | ||
self.accessKeyId = creds.AccessKeyId; | ||
self.secretAccessKey = creds.SecretAccessKey; | ||
self.sessionToken = creds.Token; | ||
self.expireTime = new Date(creds.Expiration); | ||
data = JSON.parse(data); | ||
if (self.credsFormatIsValid(data)) { | ||
var creds = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not 100% certain since I haven't tested this myself, but if these fields aren't assigned to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assigning these fields to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realized I misunderstood your question. It is indeed necessary to assign these fields to |
||
expired: false, | ||
accessKeyId: data.AccessKeyId, | ||
secretAccessKey: data.SecretAccessKey, | ||
sessionToken: data.Token, | ||
expireTime: new Date(data.Expiration) | ||
}; | ||
} else { | ||
throw AWS.util.error( | ||
new Error('Response data is not in valid format'), | ||
|
@@ -136,7 +158,7 @@ AWS.ECSCredentials = AWS.util.inherit(AWS.Credentials, { | |
err = dataError; | ||
} | ||
} | ||
callback(err); | ||
callbacks(err, creds); | ||
}); | ||
} | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -793,6 +793,51 @@ var util = { | |
if (typeof service !== 'string') service = service.serviceIdentifier; | ||
if (typeof service !== 'string' || !metadata.hasOwnProperty(service)) return false; | ||
return !!metadata[service].dualstackAvailable; | ||
}, | ||
|
||
/** | ||
* @api private | ||
*/ | ||
calculateRetryDelay: function calculateRetryDelay(retryCount, retryDelayOptions) { | ||
if (!retryDelayOptions) retryDelayOptions = {}; | ||
var customBackoff = retryDelayOptions.customBackoff || null; | ||
if (typeof customBackoff === 'function') { | ||
return customBackoff(retryCount); | ||
} | ||
var base = retryDelayOptions.base || 30; | ||
var delay = Math.random() * (Math.pow(2, retryCount) * base); | ||
return delay; | ||
}, | ||
|
||
/** | ||
* @api private | ||
*/ | ||
handleRequestWithTimeoutRetries: function handleRequestWithTimeoutRetries(httpRequest, options, cb) { | ||
if (!options) options = {}; | ||
var http = AWS.HttpClient.getInstance(); | ||
var httpOptions = options.httpOptions || {}; | ||
var retryCount = 0; | ||
|
||
var errCallback = function(err) { | ||
var maxRetries = options.maxRetries || 0; | ||
if (err && err.code === 'TimeoutError' && retryCount < maxRetries) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered retrying for all the codes we retry when a service requests fails? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've updated the PR to retry for 5xx status codes, as well as for 4xx status codes only if there is a retry-after header. I was able to reliably replicate the Metadata Service returning 429 (Too Many Requests) errors, which also returned a retry-after header which give a minimum number of seconds to wait before retrying. |
||
retryCount++; | ||
var delay = util.calculateRetryDelay(retryCount, options.retryDelayOptions); | ||
setTimeout(sendRequest, delay); | ||
} else { | ||
cb(err); | ||
} | ||
}; | ||
|
||
var sendRequest = function() { | ||
var data = ''; | ||
http.handleRequest(httpRequest, httpOptions, function(httpResponse) { | ||
httpResponse.on('data', function(chunk) { data += chunk.toString(); }); | ||
httpResponse.on('end', function() { cb(null, data); }); | ||
}, errCallback); | ||
}; | ||
|
||
process.nextTick(sendRequest); | ||
} | ||
|
||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default used to be 30 ms but was changed to 100 ms with the configurable retry delays.