UNPKG

@amazon-dax-sdk/client-dax

Version:

Amazon DAX Client for JavaScript

695 lines (577 loc) 24.4 kB
/* * 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;