amazon-dax-client
Version:
Amazon DAX Client for JavaScript
565 lines (469 loc) • 18.4 kB
JavaScript
/*
* Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License
* is located at
*
* http://aws.amazon.com/apache2.0/
*
* or in the "license" file accompanying this file. This file is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
'use strict';
/**
* kerent - 3/29/21
* antlr4 v3.8 has a circular dependency bug described here: https://github.com/antlr/antlr4/issues/2834
* This issue is resolved in antlr4 v3.9, however, it requires a NodeJS version >= 14.x
* The AWS NodeJS SDK currently allows NodeJS version >= 10.x.
* At this time, we cannot update our dependencies so we will silence this warning instead of confusing customers.
* This fix will prevent us outputting warnings from our dependencies, but not prevent users from seeing warnings that we issue to them.
* Note: We should be testing locally with the line commented out so we don't miss warnings from dependencies.
*/
process.removeAllListeners('warning');
const EventEmitter = require('events');
const Cluster = require('./Cluster');
const DaxClient = require('./DaxClient');
const DaxClientError = require('./DaxClientError');
const DaxErrorCode = require('./DaxErrorCode');
const AWS = require('aws-sdk');
const jmespath = require('jmespath');
const ERROR_CODES_WRITE_FAILURE_AMBIGUOUS = [[1, 37, 38, 53], [1, 37, 38, 55], ['*', 37, '*', 39, 47]];
const ERROR_CODES_THROTTLING = [
DaxErrorCode.ProvisionedThroughputExceeded,
DaxErrorCode.LimitExceeded,
DaxErrorCode.RequestLimitExceeded,
DaxErrorCode.Throttling,
];
// Shim class to work with inheirtance model expected by DocumentClient
const _AmazonDaxClient = AWS.util.inherit({
constructor: function AmazonDaxClient(config, cluster) {
if(!config) {
config = AWS.config;
} else {
let localConfig = config;
config = new AWS.Config(AWS.config);
config.update(localConfig, true); // Allow the use of unknown keys
}
this.config = config;
// no redirects in DAX
this.config.maxRedirects = 0;
/*
* Skip hostname verification of TLS connections. This has no impact on un-encrypted clusters.
* The default is to perform hostname verification, setting this to True will skip verification.
* Be sure you understand the implication of turning it off, which is the inability to authenticate the cluster that you are connecting to.
* The value for this configuration is a Boolean, either True or False.
*/
this.skipHostnameVerification = config.skipHostnameVerification != null ? config.skipHostnameVerification : false;
// There should be a better way than this, but the SDK API loading methods are internal
this.api = AWS.util.copy(new AWS.DynamoDB().api);
let requestTimeout = config.requestTimeout || 60000;
this._cluster = cluster ? cluster : new Cluster(config, {createDaxClient: (pool, region, el) => {
return new DaxClient(pool, region, el, requestTimeout);
}});
// precedence: write/read retry > ddb maxRetries > AWS config maxRetries > default(1 same as JAVA)
if(!config.maxRetries && config.maxRetries !== 0) {
config.maxRetries = 1;
}
this._writeRetries = config.writeRetries ? config.writeRetries : config.maxRetries;
this._readRetries = config.readRetries ? config.readRetries : config.maxRetries;
this._maxRetryDelay = config.maxRetryDelay ? config.maxRetryDelay : 7000;
this._readClientFactory = {getClient: (previous) => {
return this._cluster.readClient(previous);
}};
this._writeClientFactory = {getClient: (previous) => {
return this._cluster.leaderClient(previous);
}};
this._cluster.startup();
this._readOperationsRetryHandler = new RetryHandler(this._cluster, this._maxRetryDelay, this._readRetries);
this._writeOperationsRetryHandler = new WriteOperationsRetryHandler(this._cluster, this._maxRetryDelay, this._writeRetries);
},
shutdown: function shutdown() {
this._cluster.close();
},
// vv Supported DDB methods vv
batchGetItem: function batchGetItem(params, callback) {
return this._makeReadRequestWithRetries('batchGetItem', params, (client, newParams) => {
return client.batchGetItem(newParams);
}, callback);
},
batchWriteItem: function batchWriteItem(params, callback) {
return this._makeWriteRequestWithRetries('batchWriteItem', params, (client, newParams) => {
return client.batchWriteItem(newParams);
}, callback);
},
deleteItem: function deleteItem(params, callback) {
return this._makeWriteRequestWithRetries('deleteItem', params, (client, newParams) => {
return client.deleteItem(newParams);
}, callback);
},
getItem: function getItem(params, callback) {
return this._makeReadRequestWithRetries('getItem', params, (client, newParams) => {
return client.getItem(newParams);
}, callback);
},
putItem: function putItem(params, callback) {
return this._makeWriteRequestWithRetries('putItem', params, (client, newParams) => {
return client.putItem(newParams);
}, callback);
},
query: function query(params, callback) {
return this._makeReadRequestWithRetries('query', params, (client, newParams) => {
return client.query(newParams);
}, callback);
},
scan: function scan(params, callback) {
return this._makeReadRequestWithRetries('scan', params, (client, newParams) => {
return client.scan(newParams);
}, callback);
},
transactGetItems: function transactGetItems(params, callback) {
return this._makeReadRequestWithRetries('transactGetItems', params, (client, newParams) => {
return client.transactGetItems(newParams);
}, callback);
},
updateItem: function updateItem(params, callback) {
return this._makeWriteRequestWithRetries('updateItem', params, (client, newParams) => {
return client.updateItem(newParams);
}, callback);
},
// vv Unsupported DDB methods vv
createTable: function createTable(params, callback) {
throw new DaxClientError('createTable is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
deleteTable: function deleteTable(params, callback) {
throw new DaxClientError('deleteTable is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
describeLimits: function describeLimits(params, callback) {
throw new DaxClientError('describeLimits is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
describeTable: function describeTable(params, callback) {
throw new DaxClientError('describeTable is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
describeTimeToLive: function describeTimeToLive(params, callback) {
throw new DaxClientError('describeTimeToLive is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
listTables: function listTables(params, callback) {
throw new DaxClientError('listTables is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
listTagsOfResources: function listTagsOfResources(params, callback) {
throw new DaxClientError('listTagsOfResources is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
tagResource: function tagResource(params, callback) {
throw new DaxClientError('tagResource is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
transactWriteItems: function transactWriteItems(params, callback) {
return this._makeWriteRequestWithRetries('transactWriteItems', params, (client, newParams) => {
return client.transactWriteItems(newParams);
}, callback);
},
untagResource: function untagResource(params, callback) {
throw new DaxClientError('untagResource is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
updateTable: function updateTable(params, callback) {
throw new DaxClientError('updateTable is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
updateTimeToLive: function updateTimeToLive(params, callback) {
throw new DaxClientError('updateTimeToLive is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
waitFor: function waitFor(state, params, callback) {
throw new DaxClientError('waitFor is not support for DAX. Use AWS.DynamoDB instead.', DaxErrorCode.Validation, false);
},
// vv Private methods vv
makeRequest: function makeRequest(operation, params, callback) {
return this[operation](params, callback);
},
/**
* @api private
*/
numRetries: function numRetries() {
// ** Copied from JS SDK **
if(this.config.maxRetries !== undefined && this.config.maxRetries !== null) {
return this.config.maxRetries;
} else {
return this.defaultRetryCount;
}
},
/**
* @api private
*/
paginationConfig: function paginationConfig(operation, throwException) {
// ** Copied from JS SDK **
let paginator = this.api.operations[operation].paginator;
if(!paginator) {
if(throwException) {
let e = new Error();
throw AWS.util.error(e, 'No pagination configuration for ' + operation);
}
return null;
}
return paginator;
},
/**
* @api private
*/
_makeReadRequestWithRetries: function _makeReadRequestWithRetries(opname, params, operation, callback) {
let request = new DaxRequest(this, opname, params,
(newParams) => this._readOperationsRetryHandler.makeRequestWithRetries(
operation, newParams, this._readClientFactory, this._readRetries)
);
if(callback && typeof(callback) == 'function') {
request.send(callback);
}
return request;
},
/**
* @api private
*/
_makeWriteRequestWithRetries: function _makeWriteRequestWithRetries(opname, params, operation, callback) {
let request = new DaxRequest(this, opname, params,
(newParams) => this._writeOperationsRetryHandler.makeRequestWithRetries(
operation, newParams, this._writeClientFactory, this._writeRetries)
);
if(callback && typeof(callback) == 'function') {
request.send(callback);
}
return request;
},
});
// Exists only to work with DocumentClient
const AmazonDaxClient = AWS.util.inherit(_AmazonDaxClient, {});
class RetryHandler {
constructor(cluster, retryDelay, retries) {
this._cluster = cluster;
this._maxRetryDelay = retryDelay;
this._maxRetries = retries;
}
makeRequestWithRetries(operation, params, clientFactory, retries, prevClient) {
let newClient;
return new Promise((resolve, reject) => {
newClient = clientFactory.getClient(prevClient);
return resolve(newClient);
}).then((newClient) => {
return operation(newClient, params);
}).catch((err) => {
if(this._cluster.startupComplete() === false && err.code === DaxErrorCode.NoRoute) {
retries++;
}
if(retries <= 0) {
return Promise.reject(this.check(err));
}
if(!this.isRetryable(err)) {
return Promise.reject(this.check(err));
}
let maybeWait;
if(err.code === DaxErrorCode.NoRoute) {
maybeWait = this.waitForRoutesRebuilt();
} else {
maybeWait = this.isWaitForClusterRecoveryBeforeRetrying(err) ?
this._cluster.waitForRecovery(this._cluster._leaderSessionId, this._maxRetryDelay) :
Promise.resolve();
}
const retryHandler = () => {
return this._exponentialBackOff(err, this._maxRetries - retries).then(() => {
return this.makeRequestWithRetries(operation, params, clientFactory, retries - 1, newClient);
});
};
return maybeWait.then(
retryHandler, retryHandler // this is handler for wait fail
);
});
}
_exponentialBackOff(err, n) {
if(!ERROR_CODES_THROTTLING.includes(err.code)) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, this._jitter(70 << n));
});
}
_jitter(interval) { // jitter between 50% and 100% of interval
return interval * (0.5 + Math.random() * 0.5);
}
waitForRoutesRebuilt() {
return this._cluster.waitForRoutesRebuilt(false);
}
check(err) {
if(!err) {
err = new Error('No cluster endpoints available');
}
return err;
}
isWaitForClusterRecoveryBeforeRetrying(err) {
// normal system error won't have this property and will return false, which means no need to wait
return err.waitForRecoveryBeforeRetrying;
}
isRetryable(err) {
// only some Dax/DDB error is not retryable, will be init when creating
// only explicitly indicate that retryable is false otherwise retryable
// all non-DaxClientErrors are non-retryable
return err instanceof DaxClientError ? err.retryable !== false : false;
}
}
class WriteOperationsRetryHandler extends RetryHandler {
constructor(cluster, retryDelay, retries) {
super(cluster, retryDelay, retries);
}
isRetryable(err) {
if(this._isWriteFailureAmbiguous(err)) {
return false;
}
return super.isRetryable(err);
}
/**
* Returns true when with the given information it can't be determined if the written values is
* persisted to the data store or not. Returns false if given exception type means that data
* is not persisted to data store.
* @param {Error} err
* @return {boolean}
*/
_isWriteFailureAmbiguous(err) {
if(err.code === DaxErrorCode.Decoder
|| err.code === DaxErrorCode.MalformedResult
|| err.code === DaxErrorCode.EndOfStream
|| err.code === 'EPIPE') {
return true;
}
if(err.codeSeq && this._listContain(err.codeSeq, ERROR_CODES_WRITE_FAILURE_AMBIGUOUS)) {
return true;
}
if(err instanceof ReferenceError || err instanceof TypeError) {
return true;
}
return false;
}
_listContain(targetList, lists) {
checkList: for(let list of lists) {
if(list.length !== targetList.length) {
continue;
}
for(let i = 0; i < list.length; ++i) {
if(list[i] !== '*' && list[i] !== targetList[i]) {
continue checkList;
}
}
return true;
}
return false;
}
check(err) {
if(this._isWriteFailureAmbiguous(err)) {
let newError = new Error('Write operation failed without negative acknowledgement ');
// err.stack = newError.stack + '\n' + err.stack;
err.message = newError.message + '\n' + err.message;
return err;
}
return super.check(err);
}
}
class DaxRequest extends EventEmitter {
constructor(service, opname, params, op) {
super();
this.service = service;
this.operation = opname;
this.params = params;
this.response = new AWS.Response(this);
this.startTime = AWS.util.date.getDate();
this._op = op;
this._fired = false;
// add a no-op listeners so that validate is an array
// only needed for DocumentClient
this.addListener('validate', () => {});
this.addListener('validate', () => {});
}
abort() {
// no-op, can't abort DAX calls
return this;
}
createReadStream() {
throw new DaxClientError('createReadStream is not supported in DAX.', DaxErrorCode.Validation, false);
}
eachPage(callback) {
// ** Copied from JS SDK **
// Make all callbacks async-ish
callback = AWS.util.fn.makeAsync(callback, 3);
function wrappedCallback(response) {
callback.call(response, response.error, response.data, function(result) {
if(result === false) {
return;
}
if(response.hasNextPage()) {
response.nextPage().on('complete', wrappedCallback).send();
} else {
callback.call(response, null, null, AWS.util.fn.noop);
}
});
}
this.on('complete', wrappedCallback).send();
}
eachItem(callback) {
// ** Copied from JS SDK **
let self = this;
function wrappedCallback(err, data) {
if(err) {
return callback(err, null);
}
if(data === null) {
return callback(null, null);
}
let config = self.service.paginationConfig(self.operation);
let resultKey = config.resultKey;
if(Array.isArray(resultKey)) {
resultKey = resultKey[0];
}
let items = jmespath.search(data, resultKey);
let continueIteration = true;
AWS.util.arrayEach(items, function(item) {
continueIteration = callback(null, item);
if(continueIteration === false) {
return AWS.util.abort;
}
});
return continueIteration;
}
this.eachPage(wrappedCallback);
}
isPageable() {
// ** Copied from JS SDK **
return this.service.paginationConfig(this.operation) ? true : false;
}
promise() {
if(this._fired) {
// Request object can only be used once
throw new DaxClientError('Request object already used.', DaxErrorCode.Validation, false);
}
this._fired = true;
let self = this;
this.emit('validate', this);
// skip 'build' and 'sign' as they are not meaningful for DAX
let resultP = this._op(this.params).then((data) => {
self.response.data = data;
self.emit('extractData', self.response);
self.emit('success', self.response);
}, (err) => {
self.response.error = err;
self.emit('extractError', self.response);
self.emit('error', self.response.error, self.response);
}).then(() => {
self.emit('complete', self.response);
return self.response.data;
});
return resultP;
}
send(callback) {
if(this._fired) {
throw new DaxClientError('Request object already used.', DaxErrorCode.Validation, false);
}
let resultP = this.promise();
if(callback) {
resultP.then(
(data) => callback(null, data),
(err) => callback(err, null));
}
}
}
module.exports = AmazonDaxClient;