UNPKG

amazon-dax-client

Version:

Amazon DAX Client for JavaScript

565 lines (469 loc) 18.4 kB
/* * 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;