Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 9dddca3

Browse files
committed
Configure IAM token service retry behaviour.
1 parent f3839b4 commit 9dddca3

File tree

5 files changed

+114
-41
lines changed

5 files changed

+114
-41
lines changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ All other configuration is plugin specific. It must be passed within an object
355355
to the `plugins` parameter in the client constructor. For example:
356356

357357
```js
358-
var cloudant = new Cloudant({ url: myurl, maxAttempt: 5, plugins: [ 'iamauth', { retry: { retryDelayMultiplier: 4 } } ]);
358+
var cloudant = new Cloudant({ url: myurl, maxAttempt: 5, plugins: [ { iamauth: { iamApiKey: 'abcxyz' } }, { retry: { retryDelayMultiplier: 4 } } ]);
359359
```
360360
361361
`maxAttempt` can _not_ be overridden by plugin specific configuration.
@@ -397,9 +397,31 @@ var cloudant = new Cloudant({ url: myurl, maxAttempt: 5, plugins: [ 'iamauth', {
397397
override this. To authenticate with the IAM token service set `iamClientId`
398398
and `iamClientSecret` in your plugin configuration.
399399
400+
The plugin will retry failed requests to the token service (specifically
401+
`429` and `5xx` responses) until the number of retry requests reaches
402+
`maxAttempt`. Be aware that retrying requests to the token service delays the
403+
client request. It also increases the number of token exchange attempts and
404+
therefore may result in rate limiting by the IAM token service.
405+
406+
The retry behavior can be configured using the following options:
407+
408+
- `retryDelayMultiplier`
409+
410+
The multiplication factor used for increasing the timeout after each
411+
subsequent attempt _(default: 2)_.
412+
413+
- `retryInitialDelayMsecs`
414+
415+
The initial retry delay in milliseconds _(default: 500)_.
416+
417+
If the IAM token cannot be retrieved (either because the IAM token service is
418+
down or the IAM API key is incorrect) then the request will continue without
419+
IAM authentication allowing the database to return a `401` response to the
420+
caller so that it may be handled appropriately.
421+
400422
For example:
401423
```js
402-
var cloudant = new Cloudant({ url: 'https://examples.cloudant.com', plugins: { iamauth: { iamApiKey: 'xxxxxxxxxx' } } });
424+
var cloudant = new Cloudant({ url: 'https://examples.cloudant.com', plugins: { iamauth: { iamApiKey: 'xxxxxxxxxx', retryDelayMultiplier: 4 } } });
403425
```
404426
405427
See [IBM Cloud Identity and Access Management](https://console.bluemix.net/docs/services/Cloudant/guides/iam.html#ibm-cloud-identity-and-access-management) for more information.

lib/client.js

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2017, 2018 IBM Corp. All rights reserved.
1+
// Copyright © 2017, 2019 IBM Corp. All rights reserved.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -312,21 +312,19 @@ class CloudantClient {
312312
request.state.sending = false;
313313

314314
debug(`Request attempt: ${request.state.attempt}`);
315-
316-
utils.runHooks('onRequest', request, request.options, function(err) {
317-
utils.processState(request, function(stop) {
318-
if (request.state.retry) {
319-
debug('The onRequest hook issued retry.');
320-
return done();
321-
}
322-
if (stop) {
323-
debug(`The onRequest hook issued abort: ${stop}`);
324-
return done(stop);
325-
}
326-
327-
debug(`Delaying request for ${request.state.retryDelayMsecs} Msecs.`);
328-
329-
setTimeout(function() {
315+
debug(`Delaying request for ${request.state.retryDelayMsecs} Msecs.`);
316+
317+
setTimeout(function() {
318+
utils.runHooks('onRequest', request, request.options, function(err) {
319+
utils.processState(request, function(stop) {
320+
if (request.state.retry) {
321+
debug('The onRequest hook issued retry.');
322+
return done();
323+
}
324+
if (stop) {
325+
debug(`The onRequest hook issued abort: ${stop}`);
326+
return done(stop);
327+
}
330328
if (request.abort) {
331329
debug('Client issued abort during plugin execution.');
332330
return done(new Error('Client issued abort'));
@@ -354,9 +352,9 @@ class CloudantClient {
354352
.pipe(concatStream);
355353
}
356354
}
357-
}, request.state.retryDelayMsecs);
355+
});
358356
});
359-
});
357+
}, request.state.retryDelayMsecs);
360358
}, function(err) { debug(err.message); });
361359

362360
return request.clientStream; // return stream to client

plugins/iamauth.js

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,31 +25,29 @@ const BasePlugin = require('./base.js');
2525
*/
2626
class IAMPlugin extends BasePlugin {
2727
constructor(client, cfg) {
28-
super(client, cfg);
28+
if (typeof cfg.iamApiKey === 'undefined') {
29+
throw new Error('Missing IAM API key from configuration');
30+
}
2931

30-
var self = this;
31-
self.iamApiKey = null;
32-
self.baseUrl = cfg.baseUrl || null;
33-
self.cookieJar = request.jar();
34-
self.tokenUrl = cfg.iamTokenUrl || 'https://iam.cloud.ibm.com/identity/token';
32+
// token service retry configuration
33+
cfg = Object.assign({
34+
retryDelayMultiplier: 2,
35+
retryInitialDelayMsecs: 500
36+
}, cfg);
3537

36-
// Specifies whether IAM authentication should be applied to the request being intercepted.
37-
self.shouldApplyIAMAuth = true;
38-
self.refreshRequired = true;
38+
super(client, cfg);
3939

40-
if (typeof cfg.iamApiKey === 'undefined') {
41-
debug('Missing IAM API key. Skipping IAM authentication.');
42-
self.shouldApplyIAMAuth = false;
43-
}
40+
this.iamApiKey = null;
41+
this.baseUrl = cfg.baseUrl || null;
42+
this.cookieJar = request.jar();
43+
this.tokenUrl = cfg.iamTokenUrl || 'https://iam.cloud.ibm.com/identity/token';
44+
this.shouldApplyIAMAuth = true;
45+
this.refreshRequired = true;
4446
}
4547

4648
onRequest(state, req, callback) {
4749
var self = this;
4850

49-
if (typeof self._cfg.iamApiKey === 'undefined') {
50-
throw new Error('Missing IAM API key from configuration');
51-
}
52-
5351
if (self._cfg.iamApiKey !== self.iamApiKey) {
5452
debug('New credentials identified. Renewing session cookie...');
5553
self.shouldApplyIAMAuth = self.refreshRequired = true;
@@ -85,6 +83,13 @@ class IAMPlugin extends BasePlugin {
8583
debug(error.message);
8684
if (self.shouldApplyIAMAuth) {
8785
state.retry = true;
86+
if (state.attempt === 1) {
87+
state.retryDelayMsecs = self._cfg.retryInitialDelayMsecs;
88+
} else {
89+
state.retryDelayMsecs *= self._cfg.retryDelayMultiplier;
90+
}
91+
} else {
92+
console.warn(`Disabling IAM authentication: ${error.message}`);
8893
}
8994
} else {
9095
req.jar = self.cookieJar; // add jar

test/plugins/iamauth.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,10 +513,10 @@ describe('#db IAMAuth Plugin', function() {
513513
});
514514

515515
it('throws error for unspecified IAM API key', function() {
516-
var cloudantClient = new Client({ plugins: 'iamauth' });
517516
assert.throws(
518517
() => {
519-
cloudantClient.request({ url: SERVER + DBNAME });
518+
/* eslint-disable no-new */
519+
new Client({ plugins: 'iamauth' });
520520
},
521521
/Missing IAM API key from configuration/,
522522
'did not throw with expected message'
@@ -557,4 +557,51 @@ describe('#db IAMAuth Plugin', function() {
557557
assert.fail(`Unexpected reject: ${err}`);
558558
});
559559
});
560+
561+
it('successfully retries request on 500 IAM token service response and returns 200 response', function(done) {
562+
if (process.env.NOCK_OFF) {
563+
this.skip();
564+
}
565+
566+
var iamMocks = nock(TOKEN_SERVER)
567+
.post('/identity/token', {
568+
'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
569+
'response_type': 'cloud_iam',
570+
'apikey': IAM_API_KEY
571+
})
572+
.times(2)
573+
.reply(500, {error: 'internal_server_error', reason: 'Internal Server Error'})
574+
.post('/identity/token', {
575+
'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
576+
'response_type': 'cloud_iam',
577+
'apikey': IAM_API_KEY
578+
})
579+
.reply(200, MOCK_IAM_TOKEN_RESPONSE);
580+
581+
var cloudantMocks = nock(SERVER)
582+
.post('/_iam_session', {access_token: MOCK_ACCESS_TOKEN})
583+
.reply(200, {ok: true}, MOCK_SET_IAM_SESSION_HEADER)
584+
.get(DBNAME)
585+
.reply(200, {doc_count: 0});
586+
587+
var cloudantClient = new Client({ maxAttempt: 3, plugins: { iamauth: { iamApiKey: IAM_API_KEY } } });
588+
var req = { url: SERVER + DBNAME, method: 'GET' };
589+
590+
var startTs = (new Date()).getTime();
591+
592+
cloudantClient.request(req, function(err, resp, data) {
593+
assert.equal(err, null);
594+
assert.equal(resp.request.headers.cookie, MOCK_IAM_SESSION);
595+
assert.equal(resp.statusCode, 200);
596+
assert.ok(data.indexOf('"doc_count":0') > -1);
597+
598+
// validate retry delay
599+
var now = (new Date()).getTime();
600+
assert.ok(now - startTs > (500 + 1000));
601+
602+
iamMocks.done();
603+
cloudantMocks.done();
604+
done();
605+
});
606+
});
560607
});

test/readmeexamples.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2018 IBM Corp. All rights reserved.
1+
// Copyright © 2018, 2019 IBM Corp. All rights reserved.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -260,10 +260,11 @@ describe('#db README Examples', function() {
260260
var cloudant = new Cloudant({
261261
url: `https://${ME}.cloudant.com`,
262262
maxAttempt: 5,
263-
plugins: [ 'iamauth', { retry: { retryDelayMultiplier: 4 } } ]
263+
plugins: [ { iamauth: { iamApiKey: 'abcxyz' } }, { retry: { retryDelayMultiplier: 4 } } ]
264264
});
265265
assert.equal(cloudant.cc._plugins.length, 2);
266266
assert.equal(cloudant.cc._plugins[0].constructor.name, 'IAMPlugin');
267+
assert.equal(cloudant.cc._plugins[0]._cfg.iamApiKey, 'abcxyz');
267268
assert.equal(cloudant.cc._plugins[1].constructor.name, 'RetryPlugin');
268269
assert.equal(cloudant.cc._plugins[1]._cfg.retryDelayMultiplier, 4);
269270
});

0 commit comments

Comments
 (0)