@cumulus/aws-client 21.0.0 → 21.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,6 +83,10 @@ for allowed params and return value.</p>
83
83
  <dt><a href="#decryptBase64String">decryptBase64String(ciphertext)</a> ⇒ <code>string</code></dt>
84
84
  <dd><p>Decrypt a KMS-encrypted string, Base 64 encoded</p>
85
85
  </dd>
86
+ <dt><a href="#applyS3Jitter">applyS3Jitter(maxJitterMs, operation)</a> ⇒</dt>
87
+ <dd><p>Introduces random jitter delay to stagger concurrent S3 operations.
88
+ This helps prevent AWS S3 SlowDown errors when many operations occur simultaneously.</p>
89
+ </dd>
86
90
  </dl>
87
91
 
88
92
  <a name="module_CloudFormation"></a>
@@ -1122,6 +1126,20 @@ Decrypt a KMS-encrypted string, Base 64 encoded
1122
1126
  | --- | --- | --- |
1123
1127
  | ciphertext | <code>string</code> | a KMS-encrypted value, Base 64 encoded |
1124
1128
 
1129
+ <a name="applyS3Jitter"></a>
1130
+
1131
+ ## applyS3Jitter(maxJitterMs, operation) ⇒
1132
+ Introduces random jitter delay to stagger concurrent S3 operations.
1133
+ This helps prevent AWS S3 SlowDown errors when many operations occur simultaneously.
1134
+
1135
+ **Kind**: global function
1136
+ **Returns**: A Promise that resolves after the random delay
1137
+
1138
+ | Param | Description |
1139
+ | --- | --- |
1140
+ | maxJitterMs | Maximum jitter time in milliseconds (0-59000). If 0, no delay is applied. |
1141
+ | operation | Optional operation name for logging context |
1142
+
1125
1143
 
1126
1144
  ## About Cumulus
1127
1145
 
package/S3.js CHANGED
@@ -46,10 +46,13 @@ const checksum_1 = require("@cumulus/checksum");
46
46
  const errors_1 = require("@cumulus/errors");
47
47
  const logger_1 = __importDefault(require("@cumulus/logger"));
48
48
  const S3MultipartUploads = __importStar(require("./lib/S3MultipartUploads"));
49
+ const s3_jitter_1 = require("./s3-jitter");
49
50
  const services_1 = require("./services");
50
51
  const test_utils_1 = require("./test-utils");
51
52
  const utils_1 = require("./utils");
52
53
  const log = new logger_1.default({ sender: 'aws-client/s3' });
54
+ // S3 jitter configuration from environment variable
55
+ const s3JitterMaxMs = Number(process.env.S3_JITTER_MAX_MS || 0);
53
56
  const buildDeprecationMessage = (name, version, alternative) => {
54
57
  let message = `${name} is deprecated after version ${version} and will be removed in a future release.`;
55
58
  if (alternative)
@@ -148,6 +151,7 @@ exports.deleteS3Objects = deleteS3Objects;
148
151
  * @returns returns response from `S3.headObject` as a promise
149
152
  **/
150
153
  const headObject = (Bucket, Key, retryOptions = { retries: 0 }) => (0, p_retry_1.default)(async () => {
154
+ await (0, s3_jitter_1.applyS3Jitter)(s3JitterMaxMs, `headObject(${Bucket}/${Key})`);
151
155
  try {
152
156
  return await (0, services_1.s3)().headObject({ Bucket, Key });
153
157
  }
@@ -212,7 +216,10 @@ exports.waitForObjectToExist = waitForObjectToExist;
212
216
  *
213
217
  * @param params - same params as https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property
214
218
  **/
215
- const s3PutObject = (params) => (0, services_1.s3)().putObject(params);
219
+ const s3PutObject = async (params) => {
220
+ await (0, s3_jitter_1.applyS3Jitter)(s3JitterMaxMs, `putObject(${params.Bucket}/${params.Key})`);
221
+ return (0, services_1.s3)().putObject(params);
222
+ };
216
223
  exports.s3PutObject = s3PutObject;
217
224
  /**
218
225
  * Upload a file to S3
@@ -227,10 +234,13 @@ exports.putFile = putFile;
227
234
  /**
228
235
  * Copy an object from one location on S3 to another
229
236
  **/
230
- const s3CopyObject = (params) => (0, services_1.s3)().copyObject({
231
- TaggingDirective: 'COPY',
232
- ...params,
233
- });
237
+ const s3CopyObject = async (params) => {
238
+ await (0, s3_jitter_1.applyS3Jitter)(s3JitterMaxMs, `copyObject(${params.Bucket}/${params.Key})`);
239
+ return (0, services_1.s3)().copyObject({
240
+ TaggingDirective: 'COPY',
241
+ ...params,
242
+ });
243
+ };
234
244
  exports.s3CopyObject = s3CopyObject;
235
245
  /**
236
246
  * Upload data to S3
@@ -238,6 +248,7 @@ exports.s3CopyObject = s3CopyObject;
238
248
  * see https://github.com/aws/aws-sdk-js-v3/tree/main/lib/lib-storage
239
249
  */
240
250
  const promiseS3Upload = async (params) => {
251
+ await (0, s3_jitter_1.applyS3Jitter)(s3JitterMaxMs, `upload(${params.params?.Bucket}/${params.params?.Key})`);
241
252
  const parallelUploads = new lib_storage_1.Upload({
242
253
  ...params,
243
254
  client: (0, services_1.s3)(),
@@ -290,6 +301,7 @@ const downloadS3File = async (s3Obj, filepath) => {
290
301
  if (!s3Obj.Bucket || !s3Obj.Key) {
291
302
  throw new Error('Bucket and Key are required');
292
303
  }
304
+ await (0, s3_jitter_1.applyS3Jitter)(s3JitterMaxMs, `downloadS3File(${s3Obj.Bucket}/${s3Obj.Key})`);
293
305
  const fileWriteStream = fs_1.default.createWriteStream(filepath);
294
306
  const objectStream = await (0, exports.getObjectReadStream)({
295
307
  bucket: s3Obj.Bucket,
@@ -309,6 +321,7 @@ exports.downloadS3File = downloadS3File;
309
321
  */
310
322
  const getObjectSize = async (params) => {
311
323
  // eslint-disable-next-line no-shadow
324
+ await (0, s3_jitter_1.applyS3Jitter)(s3JitterMaxMs, `getObjectSize(${params.bucket}/${params.key})`);
312
325
  const { s3: s3Client, bucket, key } = params;
313
326
  const headObjectResponse = await s3Client.headObject({
314
327
  Bucket: bucket,
@@ -348,7 +361,10 @@ exports.s3PutObjectTagging = s3PutObjectTagging;
348
361
  /**
349
362
  * Gets an object from S3.
350
363
  */
351
- const getObject = (s3Client, params) => s3Client.getObject(params);
364
+ const getObject = async (s3Client, params) => {
365
+ await (0, s3_jitter_1.applyS3Jitter)(s3JitterMaxMs, `getObject(${params.Bucket}/${params.Key})`);
366
+ return s3Client.getObject(params);
367
+ };
352
368
  exports.getObject = getObject;
353
369
  /**
354
370
  * Get an object from S3, waiting for it to exist and, if specified, have the
@@ -754,6 +770,7 @@ const uploadPartCopy = async (params) => {
754
770
  */
755
771
  const multipartCopyObject = async (params) => {
756
772
  const { sourceBucket, sourceKey, destinationBucket, destinationKey, ACL, copyTags = false, chunkSize, } = params;
773
+ await (0, s3_jitter_1.applyS3Jitter)(s3JitterMaxMs, `multipartCopyObject(${destinationBucket}/${destinationKey})`);
757
774
  const sourceObject = params.sourceObject ?? await (0, exports.headObject)(sourceBucket, sourceKey);
758
775
  // Create a multi-part upload (copy) and get its UploadId
759
776
  const uploadId = await createMultipartUpload({
package/SNS.js CHANGED
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createSnsTopic = exports.publishSnsMessageWithRetry = void 0;
10
10
  const p_retry_1 = __importDefault(require("p-retry"));
11
11
  const logger_1 = __importDefault(require("@cumulus/logger"));
12
+ const errors_1 = require("@cumulus/errors");
12
13
  const client_sns_1 = require("@aws-sdk/client-sns");
13
14
  const services_1 = require("./services");
14
15
  const log = new logger_1.default({ sender: 'aws-client/sns' });
@@ -33,7 +34,7 @@ const publishSnsMessageWithRetry = async (snsTopicArn, message, retryOptions = {
33
34
  await (0, services_1.sns)().send(new client_sns_1.PublishCommand(publishInput));
34
35
  }, {
35
36
  maxTimeout: 5000,
36
- onFailedAttempt: (err) => log.debug(`publishSnsMessageWithRetry('${snsTopicArn}', '${JSON.stringify(message)}') failed with ${err.retriesLeft} retries left: ${JSON.stringify(err)}`),
37
+ onFailedAttempt: (err) => log.debug(`publishSnsMessageWithRetry('${snsTopicArn}', '${JSON.stringify(message)}') failed with ${err.retriesLeft} retries left: ${(0, errors_1.errorify)(err)}`),
37
38
  ...retryOptions,
38
39
  });
39
40
  exports.publishSnsMessageWithRetry = publishSnsMessageWithRetry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cumulus/aws-client",
3
- "version": "21.0.0",
3
+ "version": "21.1.0",
4
4
  "description": "Utilities for working with AWS",
5
5
  "keywords": [
6
6
  "GIBS",
@@ -69,10 +69,10 @@
69
69
  "@aws-sdk/s3-request-presigner": "^3.621.0",
70
70
  "@aws-sdk/signature-v4-crt": "^3.621.0",
71
71
  "@aws-sdk/types": "^3.609.0",
72
- "@cumulus/checksum": "21.0.0",
73
- "@cumulus/errors": "21.0.0",
74
- "@cumulus/logger": "21.0.0",
75
- "@cumulus/types": "21.0.0",
72
+ "@cumulus/checksum": "21.1.0",
73
+ "@cumulus/errors": "21.1.0",
74
+ "@cumulus/logger": "21.1.0",
75
+ "@cumulus/types": "21.1.0",
76
76
  "lodash": "~4.17.21",
77
77
  "mem": "^8.0.2",
78
78
  "p-map": "^1.2.0",
@@ -83,8 +83,8 @@
83
83
  "uuid": "^8.2.0"
84
84
  },
85
85
  "devDependencies": {
86
- "@cumulus/test-data": "21.0.0",
86
+ "@cumulus/test-data": "21.1.0",
87
87
  "@types/uuid": "^8.0.0"
88
88
  },
89
- "gitHead": "19bb3477969662a9e0b300f10f6df23b6c0654db"
89
+ "gitHead": "5d443a04647ed537903c85b48992d08ce3c3cd1d"
90
90
  }
package/s3-jitter.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Introduces random jitter delay to stagger concurrent S3 operations.
3
+ * This helps prevent AWS S3 SlowDown errors when many operations occur simultaneously.
4
+ *
5
+ * @param maxJitterMs - Maximum jitter time in milliseconds (0-59000).
6
+ * If 0, no delay is applied.
7
+ * @param operation - Optional operation name for logging context
8
+ * @returns A Promise that resolves after the random delay
9
+ */
10
+ export declare const applyS3Jitter: (maxJitterMs: number, operation?: string) => Promise<void>;
11
+ //# sourceMappingURL=s3-jitter.d.ts.map
package/s3-jitter.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.applyS3Jitter = void 0;
7
+ const logger_1 = __importDefault(require("@cumulus/logger"));
8
+ const log = new logger_1.default({ sender: 's3-jitter' });
9
+ /**
10
+ * Introduces random jitter delay to stagger concurrent S3 operations.
11
+ * This helps prevent AWS S3 SlowDown errors when many operations occur simultaneously.
12
+ *
13
+ * @param maxJitterMs - Maximum jitter time in milliseconds (0-59000).
14
+ * If 0, no delay is applied.
15
+ * @param operation - Optional operation name for logging context
16
+ * @returns A Promise that resolves after the random delay
17
+ */
18
+ const applyS3Jitter = async (maxJitterMs, operation) => {
19
+ if (maxJitterMs <= 0) {
20
+ return;
21
+ }
22
+ const jitterMs = Math.ceil(Math.random() * maxJitterMs);
23
+ const logContext = operation ? ` for ${operation}` : '';
24
+ log.info(`Applying S3 jitter: ${jitterMs}ms${logContext} (max: ${maxJitterMs}ms)`);
25
+ await new Promise((resolve) => setTimeout(resolve, jitterMs));
26
+ log.debug(`S3 jitter delay completed: ${jitterMs}ms${logContext}`);
27
+ };
28
+ exports.applyS3Jitter = applyS3Jitter;
29
+ //# sourceMappingURL=s3-jitter.js.map