@nerdware/ddb-single-table
Version:
A schema-based DynamoDB modeling tool, high-level API, and type-generator built to supercharge single-table designs!⚡
89 lines (88 loc) • 4.71 kB
JavaScript
import { isString, isArray } from "@nerdware/ts-type-safety-utils";
import { DdbSingleTableError } from "../utils/errors.js";
/**
* This DynamoDB batch-requests helper handles submission and retry logic for batch
* operations like `BatchGetItem` and `BatchWriteItem`.
*
* It works by recursively invoking the provided `submitBatchRequest` function, which
* must return any `UnprocessedItems` or `UnprocessedKeys` from the DDB batch operation.
*
* > To disable the delay between retries, set `initialDelay` to `0`.
*
* ### **Exponential Backoff Strategy:**
*
* 1. First request: no delay
* 2. Second request: delay `initialDelay` milliseconds (default: `100`)
* 3. All subsequent request delays are equal to the previous delay multiplied by the
* `timeMultiplier` (default: `2`), until either:
* - The `maxRetries` limit is reached (default: `10`), or
* - The `maxDelay` limit is reached (default: `3500`, or 3.5 seconds)
*
* Ergo, the base `delay` calculation can be summarized as follows:
* > `initialDelay * timeMultiplier^attemptNumber milliseconds`
*
* If `useJitter` is true (default: `false`), the `delay` is randomized by applying
* the following to the base `delay`: `Math.round(Math.random() * delay)`. Note that
* the determination as to whether the delay exceeds the `maxDelay` is made BEFORE
* jitter is applied.
*
* @param submitBatchRequest A fn which invokes a DDB batch operation and returns any `UnprocessedItems`/`UnprocessedKeys`.
* @param batchRequestObjects The array of request objects to submit via the batch operation.
* @param retryConfigs Configs for the retry strategy.
* @param attemptNumber The current attempt number.
*/
export const batchRequestWithExponentialBackoff = async (submitBatchRequest, batchRequestObjects, retryConfigs = {}, numPreviousRetries = 0) => {
const { disableDelay = false, initialDelay = 100, timeMultiplier = 2, useJitter = false, maxRetries = 10, maxDelay = 3500, shouldThrowOnConstraintViolation = false, } = retryConfigs;
// Init variable to hold UnprocessedItems/UnprocessedKeys
let unprocessedRequestObjects;
try {
// Submit the batch request
unprocessedRequestObjects = await submitBatchRequest(batchRequestObjects);
}
catch (err) {
// If a batch op throws, NONE of the requests were successful, check if `err.Code` is retryable.
const maybeErrCode = err?.Code;
if (!isString(maybeErrCode) || !RETRYABLE_BATCH_ERROR_CODES[maybeErrCode])
throw err;
// If `err.Code` indicates the op should be retried, run again with all batchRequestObjects.
unprocessedRequestObjects = batchRequestObjects;
}
if (isArray(unprocessedRequestObjects) && unprocessedRequestObjects.length > 0) {
// Determine the next `numPreviousRetries` and the delay before the next attempt
const retryCount = numPreviousRetries + 1;
// If not disabled, the delay = initialDelay * timeMultiplier^retryCount milliseconds
let delay = disableDelay ? 0 : initialDelay * timeMultiplier ** retryCount;
// If the next attempt would exceed maxRetries OR maxDelay, stop retrying
if (retryCount > maxRetries || delay > maxDelay) {
if (shouldThrowOnConstraintViolation)
throw new DdbSingleTableError(`After ${numPreviousRetries} attempts, ${unprocessedRequestObjects.length} batch requests `
+ `were still unable to be processed due to insufficient provisioned throughput.`);
return unprocessedRequestObjects;
}
// Apply "randomness" to the delay if `useJitter` is true
if (useJitter)
delay = Math.round(Math.random() * delay);
// Wait `delay` milliseconds, then retry the operation with the unprocessed items
await new Promise((resolve) => {
setTimeout(resolve, delay);
});
// Recursive retries
return await batchRequestWithExponentialBackoff(submitBatchRequest, unprocessedRequestObjects, retryConfigs, retryCount);
}
};
/**
* A map of {@link BatchErrorCode|DDB batch-statement error codes}
* which indicate a batch request should be retried.
*
* > See [DynamoDB — Error messages and codes][docs]
* >
* > Note: these error codes are all HTTP 400 errors.
*
* [docs]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html#Programming.Errors.MessagesAndCodes
*/
const RETRYABLE_BATCH_ERROR_CODES = {
// Applicable to PROVISIONED BillingMode:
ProvisionedThroughputExceeded: true,
// Applicable to PAY_PER_REQUEST BillingMode:
RequestLimitExceeded: true,
};