@amazon-dax-sdk/client-dax
Version:
Amazon DAX Client for JavaScript
695 lines (577 loc) • 24.4 kB
JavaScript
/*
* Copyright 2024 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';
process.removeAllListeners('warning');
const EventEmitter = require('events');
const Cluster = require('./Cluster');
const DaxClient = require('./DaxClient');
const DaxClientError = require('./DaxClientError');
const DaxErrorCode = require('./DaxErrorCode');
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,
];
// This is the source class which is used to instantiate the Dax objects.
class Dax {
constructor(config, cluster) {
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;
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);
this.initializeMiddlewareStackException();
}
initializeMiddlewareStackException() {
const createMiddlewareError = () => {
throw new DaxClientError('custom middleware and middlewareStacks are not supported for DAX. Please refer to documentation.', DaxErrorCode.Validation, false);
};
const middlewareMethods = [
'add',
'addRelativeTo',
'clone',
'concat',
'identify',
'identifyOnResolve',
'remove',
'removeByTag',
'resolve',
'use',
];
this.middlewareStack = middlewareMethods.reduce((acc, method) => {
acc[method] = createMiddlewareError;
return acc;
}, {});
}
shutdown() {
this._cluster.close();
}
// vv Supported DDB methods vv
batchGetItem(params, optionsOrCb, callback) {
if(typeof optionsOrCb === 'function') {
callback = optionsOrCb;
optionsOrCb = undefined;
}
return this._makeReadRequestWithRetries('batchGetItem', params, (client, newParams) => {
return client.batchGetItem(newParams);
}, callback);
}
batchWriteItem(params, optionsOrCb, callback) {
if(typeof optionsOrCb === 'function') {
callback = optionsOrCb;
optionsOrCb = undefined;
}
return this._makeWriteRequestWithRetries('batchWriteItem', params, (client, newParams) => {
return client.batchWriteItem(newParams);
}, callback);
}
deleteItem(params, optionsOrCb, callback) {
if(typeof optionsOrCb === 'function') {
callback = optionsOrCb;
optionsOrCb = undefined;
}
return this._makeWriteRequestWithRetries('deleteItem', params, (client, newParams) => {
return client.deleteItem(newParams);
}, callback);
}
getItem(params, optionsOrCb, callback) {
if(typeof optionsOrCb === 'function') {
callback = optionsOrCb;
optionsOrCb = undefined;
}
return this._makeReadRequestWithRetries('getItem', params, (client, newParams) => {
return client.getItem(newParams);
}, callback);
}
putItem(params, optionsOrCb, callback) {
if(typeof optionsOrCb === 'function') {
callback = optionsOrCb;
optionsOrCb = undefined;
}
return this._makeWriteRequestWithRetries('putItem', params, (client, newParams) => {
return client.putItem(newParams);
}, callback);
}
query(params, optionsOrCb, callback) {
if(typeof optionsOrCb === 'function') {
callback = optionsOrCb;
optionsOrCb = undefined;
}
if(params.Limit != null && params.Limit <= 0) {
throw new DaxClientError(
`Value ${params.Limit} at 'limit' failed to satisfy constraint: ` +
'Member must have value greater than or equal to 1',
DaxErrorCode.Validation,
false
);
}
return this._makeReadRequestWithRetries('query', params, (client, newParams) => {
return client.query(newParams);
}, callback);
}
scan(params, optionsOrCb, callback) {
if(typeof optionsOrCb === 'function') {
callback = optionsOrCb;
optionsOrCb = undefined;
}
if(params.Limit != null && params.Limit <= 0) {
throw new DaxClientError(
`Value ${params.Limit} at 'limit' failed to satisfy constraint: ` +
'Member must have value greater than or equal to 1',
DaxErrorCode.Validation,
false
);
}
return this._makeReadRequestWithRetries('scan', params, (client, newParams) => {
return client.scan(newParams);
}, callback);
}
transactGetItems(params, optionsOrCb, callback) {
if(typeof optionsOrCb === 'function') {
callback = optionsOrCb;
optionsOrCb = undefined;
}
return this._makeReadRequestWithRetries('transactGetItems', params, (client, newParams) => {
return client.transactGetItems(newParams);
}, callback);
}
transactWriteItems(params, optionsOrCb, callback) {
if(typeof optionsOrCb === 'function') {
callback = optionsOrCb;
optionsOrCb = undefined;
}
return this._makeWriteRequestWithRetries('transactWriteItems', params, (client, newParams) => {
return client.transactWriteItems(newParams);
}, callback);
}
updateItem(params, optionsOrCb, callback) {
if(typeof optionsOrCb === 'function') {
callback = optionsOrCb;
optionsOrCb = undefined;
}
return this._makeWriteRequestWithRetries('updateItem', params, (client, newParams) => {
return client.updateItem(newParams);
}, callback);
}
// vv Unsupported DDB methods vv
batchExecuteStatement(params, optionsOrCb, callback) {
throw new DaxClientError('batchExecuteStatement is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
createBackup(params, optionsOrCb, callback) {
throw new DaxClientError('createBackup is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
createGlobalTable(params, optionsOrCb, callback) {
throw new DaxClientError('createGlobalTable is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
createTable(params, optionsOrCb, callback) {
throw new DaxClientError('createTable is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
deleteBackup(params, optionsOrCb, callback) {
throw new DaxClientError('deleteBackup is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
deleteResourcePolicy(params, optionsOrCb, callback) {
throw new DaxClientError('deleteResourcePolicy is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
deleteTable(params, optionsOrCb, callback) {
throw new DaxClientError('deleteTable is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeBackup(params, optionsOrCb, callback) {
throw new DaxClientError('describeBackup is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeContinuousBackups(params, optionsOrCb, callback) {
throw new DaxClientError('describeContinuousBackups is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeContributorInsights(params, optionsOrCb, callback) {
throw new DaxClientError('describeContributorInsights is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeEndpoints(params, optionsOrCb, callback) {
throw new DaxClientError('describeEndpoints is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeExport(params, optionsOrCb, callback) {
throw new DaxClientError('describeExport is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeGlobalTable(params, optionsOrCb, callback) {
throw new DaxClientError('describeGlobalTable is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeGlobalTableSettings(params, optionsOrCb, callback) {
throw new DaxClientError('describeGlobalTableSettings is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeImport(params, optionsOrCb, callback) {
throw new DaxClientError('describeImport is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeKinesisStreamingDestination(params, optionsOrCb, callback) {
throw new DaxClientError('describeKinesisStreamingDestination is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeLimits(params, optionsOrCb, callback) {
throw new DaxClientError('describeLimits is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeTable(params, optionsOrCb, callback) {
throw new DaxClientError('describeTable is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeTableReplicaAutoScaling(params, optionsOrCb, callback) {
throw new DaxClientError('describeTableReplicaAutoScaling is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
describeTimeToLive(params, optionsOrCb, callback) {
throw new DaxClientError('describeTimeToLive is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
disableKinesisStreamingDestination(params, optionsOrCb, callback) {
throw new DaxClientError('disableKinesisStreamingDestination is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
enableKinesisStreamingDestination(params, optionsOrCb, callback) {
throw new DaxClientError('enableKinesisStreamingDestination is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
executeStatement(params, optionsOrCb, callback) {
throw new DaxClientError('executeStatement is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
executeTransaction(params, optionsOrCb, callback) {
throw new DaxClientError('executeTransaction is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
exportTableToPointInTime(params, optionsOrCb, callback) {
throw new DaxClientError('exportTableToPointInTime is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
getResourcePolicy(params, optionsOrCb, callback) {
throw new DaxClientError('getResourcePolicy is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
importTable(params, optionsOrCb, callback) {
throw new DaxClientError('importTable is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
listBackups(params, optionsOrCb, callback) {
throw new DaxClientError('listBackups is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
listContributorInsights(params, optionsOrCb, callback) {
throw new DaxClientError('listContributorInsights is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
listExports(params, optionsOrCb, callback) {
throw new DaxClientError('listExports is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
listGlobalTables(params, optionsOrCb, callback) {
throw new DaxClientError('listGlobalTables is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
listImports(params, optionsOrCb, callback) {
throw new DaxClientError('listImports is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
listTables(params, optionsOrCb, callback) {
throw new DaxClientError('listTables is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
listTagsOfResource(params, optionsOrCb, callback) {
throw new DaxClientError('listTagsOfResource is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
putResourcePolicy(params, optionsOrCb, callback) {
throw new DaxClientError('putResourcePolicy is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
restoreTableFromBackup(params, optionsOrCb, callback) {
throw new DaxClientError('restoreTableFromBackup is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
restoreTableToPointInTime(params, optionsOrCb, callback) {
throw new DaxClientError('restoreTableToPointInTime is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
tagResource(params, optionsOrCb, callback) {
throw new DaxClientError('tagResource is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
untagResource(params, optionsOrCb, callback) {
throw new DaxClientError('untagResource is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
updateContinuousBackups(params, optionsOrCb, callback) {
throw new DaxClientError('updateContinuousBackups is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
updateContributorInsights(params, optionsOrCb, callback) {
throw new DaxClientError('updateContributorInsights is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
updateGlobalTable(params, optionsOrCb, callback) {
throw new DaxClientError('updateGlobalTable is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
updateGlobalTableSettings(params, optionsOrCb, callback) {
throw new DaxClientError('updateGlobalTableSettings is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
updateKinesisStreamingDestination(params, optionsOrCb, callback) {
throw new DaxClientError('updateKinesisStreamingDestination is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
updateTable(params, optionsOrCb, callback) {
throw new DaxClientError('updateTable is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
updateTableReplicaAutoScaling(params, optionsOrCb, callback) {
throw new DaxClientError('updateTableReplicaAutoScaling is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
updateTimeToLive(params, optionsOrCb, callback) {
throw new DaxClientError('updateTimeToLive is not supported for DAX. Use DynamoDB instead.', DaxErrorCode.Validation, false);
}
// Unsupported DynamoDB minimal client send operation
send(command, optionsOrCb, callback) {
throw new DaxClientError('send operation is not supported for DAX. Please refer to documentation.', DaxErrorCode.Validation, false);
}
// vv Private methods vv
makeRequest(operation, params, callback) {
return this[operation](params, callback);
}
/**
* @api private
*/
numRetries() {
// ** Copied from JS SDK **
if(this.config.maxRetries !== undefined && this.config.maxRetries !== null) {
return this.config.maxRetries;
} else {
return this.defaultRetryCount;
}
}
/**
* @api private
*/
_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);
} else {
return request.send();
}
}
/**
* @api private
*/
_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);
} else {
return request.send();
}
}
}
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 = {
data: null,
error: null,
};
this.startTime = new Date();
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);
}
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));
} else {
return resultP;
}
}
}
module.exports = Dax;