lifion-kinesis
Version:
Lifion client for Amazon Kinesis Data streams
313 lines (290 loc) • 9.53 kB
JavaScript
/**
* Module that wraps the calls to the AWS DynamoDB client. Calls are wrapped so they can be
* retried with a custom logic instead of the one provided by the AWS SDK. In addition to retries,
* the call stacks are preserved even in async/await calls by using the `CAPTURE_STACK_TRACE`
* environment variable.
*
* @module dynamodb-client
* @private
*/
import retry from 'async-retry';
import { DynamoDB, waitUntilTableExists, waitUntilTableNotExists } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
import { reportError, reportResponse } from './stats.js';
import { getStackObj, shouldBailRetry, transformErrorStack } from './utils.js';
import { assertCredentialsSupported } from './aws-credentials.js';
const TABLE_WAIT_TIME_SECONDS = 300;
const statsSource = 'dynamoDb';
const stoppedClients = new WeakSet();
/**
* Calls a method on the given DynamoDB client. The call stack is preserved, and the results of the
* call are aggregated in the stats. Retries in this function are the original ones provided by the
* AWS SDK.
*
* @param {Object} client - An instance of the AWS DynamoDB client.
* @param {string} methodName - The name of the method to call.
* @param {...*} args - The arguments of the method call.
* @fulfil {*} - The original response from the AWS DynamoDB call.
* @reject {Error} - The error details from AWS DynamoDB with a corrected error stack.
* @returns {Promise}
* @private
*/
async function sdkCall(client, methodName, ...args) {
const stackObj = getStackObj(sdkCall);
try {
return client[methodName](...args)
.then((response) => {
reportResponse(statsSource);
return response;
})
.catch((err) => {
const error = transformErrorStack(err, stackObj);
reportError(statsSource, error);
throw error;
});
} catch (err) {
const error = transformErrorStack(err, stackObj);
reportError(statsSource, error);
throw error;
}
}
/**
* Calls a method on the given DynamoDB client. The call stack is preserved, and the results of the
* call are aggregated in the stats. Retries in this function are based on a custom logic replacing
* the one provided by the AWS SDK.
*
* @param {Object} client - An instance of the AWS DynamoDB client.
* @param {string} methodName - The name of the method to call.
* @param {Object} retryOpts - The [retry options as in async-retry]{@link external:AsyncRetry}.
* @param {...*} args - The argument of the method call.
* @fulfil {*} - The original response from the AWS DynamoDB call.
* @reject {Error} - The error details from AWS DynamoDB with a corrected error stack.
* @returns {Promise}
* @private
*/
function retriableSdkCall(client, methodName, retryOpts, ...args) {
const stackObj = getStackObj(retriableSdkCall);
return retry((bail) => {
if (stoppedClients.has(client)) {
bail(new Error('The DynamoDB client is stopped.'));
return undefined;
}
try {
return client[methodName](...args)
.then((response) => {
reportResponse(statsSource);
return response;
})
.catch((err) => {
const error = transformErrorStack(err, stackObj);
reportError(statsSource, error);
if (!shouldBailRetry(err)) throw error;
else bail(error);
});
} catch (err) {
const error = transformErrorStack(err, stackObj);
reportError(statsSource, error);
bail(error);
return undefined;
}
}, retryOpts);
}
/**
* A class that wraps the AWS DynamoDB client.
*
* @alias module:dynamodb-client
*/
class DynamoDbClient {
#data = {};
/**
* Initializes the AWS DynamoDB internal instance and prepares the retry logic.
*
* @param {Object} options - The initialization options.
* @param {Object} options.awsOptions - The initialization options for the AWS DynamoDB client.
* @param {Object} options.logger - An instace of a logger.
* @param {string} options.tableName - The name of the DynamoDB table.
*/
constructor({ awsOptions, logger, tableName }) {
assertCredentialsSupported(awsOptions);
const client = new DynamoDB(awsOptions);
const docClient = DynamoDBDocument.from(client, {
marshallOptions: { removeUndefinedValues: true }
});
const retryOpts = {
forever: true,
maxTimeout: 5 * 60 * 1000,
minTimeout: 1000,
onRetry: (err) => {
const { code, message, requestId, statusCode } = err;
logger.warn(
`Trying to recover from AWS DynamoDB error…\n${[
`\t- Message: ${message}`,
`\t- Request ID: ${requestId}`,
`\t- Code: ${code} (${statusCode})`,
`\t- Table: ${tableName}`
].join('\n')}`
);
},
randomize: true
};
Object.assign(this.#data, { client, docClient, logger, retryOpts, tableName });
}
/**
* Stops the client. Pending and future retriable calls bail out instead of
* retrying, so the client stops holding the event loop open.
*/
stop() {
const { client, docClient } = this.#data;
stoppedClients.add(client);
stoppedClients.add(docClient);
}
/**
* The CreateTable operation adds a new table to your account.
*
* @param {...*} args - The arguments.
* @returns {Promise}
*/
async createTable(...args) {
const { client, logger } = this.#data;
try {
await sdkCall(client, 'createTable', ...args);
return undefined;
} catch (err) {
if (err.code === 'ResourceInUseException') {
logger.debug('The table already exists.');
return undefined;
}
throw err;
}
}
/**
* Returns information about the table, including the current status of the table, when it was
* created, the primary key schema, and any indexes on the table.
*
* @param {...*} args - The arguments.
* @returns {Promise}
*/
describeTable(...args) {
const { client, retryOpts } = this.#data;
return retriableSdkCall(client, 'describeTable', retryOpts, ...args);
}
/**
* List all tags on an Amazon DynamoDB resource.
*
* @param {...*} args - The arguments.
* @returns {Promise}
*/
listTagsOfResource(...args) {
const { client, retryOpts } = this.#data;
return retriableSdkCall(client, 'listTagsOfResource', retryOpts, ...args);
}
/**
* Associate a set of tags with an Amazon DynamoDB resource.
*
* @param {...*} args - The arguments.
* @returns {Promise}
*/
async tagResource(...args) {
const { client, logger } = this.#data;
try {
await sdkCall(client, 'tagResource', ...args);
return undefined;
} catch (err) {
if (err.code === 'ResourceInUseException') {
logger.debug('Ignoring concurrent modification of resource.');
return undefined;
}
throw err;
}
}
/**
* Waits for the specified DynamoDB table to reach the given state.
*
* @param {string} state - The state to wait for, either `tableExists` or `tableNotExists`.
* @param {Object} params - The parameters identifying the table (e.g. `{ TableName }`).
* @fulfil {Object} - The last describe-table response observed by the waiter.
* @returns {Promise}
*/
async waitFor(state, params) {
const { client } = this.#data;
const stackObj = getStackObj(this.waitFor);
const waiter = state === 'tableNotExists' ? waitUntilTableNotExists : waitUntilTableExists;
try {
const { reason } = await waiter({ client, maxWaitTime: TABLE_WAIT_TIME_SECONDS }, params);
reportResponse(statsSource);
return reason;
} catch (err) {
const error = transformErrorStack(err, stackObj);
reportError(statsSource, error);
throw error;
}
}
/**
* Deletes a single item in a table by primary key.
*
* @param {...*} args - The arguments.
* @returns {Promise}
*/
delete(...args) {
const { docClient, tableName } = this.#data;
const [params, ...rest] = args;
return sdkCall(docClient, 'delete', { TableName: tableName, ...params }, ...rest);
}
/**
* Returns a set of attributes for the item with the given primary key.
*
* @param {...*} args - The arguments.
* @returns {Promise}
*/
get(...args) {
const { docClient, retryOpts, tableName } = this.#data;
const [params, ...rest] = args;
return retriableSdkCall(
docClient,
'get',
retryOpts,
{ TableName: tableName, ...params },
...rest
);
}
/**
* Creates a new item, or replaces an old item with a new item.
*
* @param {...*} args - The arguments.
* @returns {Promise}
*/
put(...args) {
const { docClient, retryOpts, tableName } = this.#data;
const [params, ...rest] = args;
return retriableSdkCall(
docClient,
'put',
retryOpts,
{ TableName: tableName, ...params },
...rest
);
}
/**
* Edits an existing item's attributes, or adds a new item to the table if it does not already
* exist.
*
* @param {...*} args - The arguments.
* @returns {Promise}
*/
update(...args) {
const { docClient, retryOpts, tableName } = this.#data;
const [params, ...rest] = args;
return retriableSdkCall(
docClient,
'update',
retryOpts,
{ TableName: tableName, ...params },
...rest
);
}
}
/**
* @external AsyncRetry
* @see https://github.com/zeit/async-retry#api
*/
export default DynamoDbClient;