@cumulus/async-operations 12.0.0 → 13.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cumulus/async-operations",
3
- "version": "12.0.0",
3
+ "version": "13.0.0",
4
4
  "description": "Cumulus Core internal async operations module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -28,18 +28,18 @@
28
28
  "author": "Cumulus Authors",
29
29
  "license": "Apache-2.0",
30
30
  "dependencies": {
31
- "@cumulus/aws-client": "12.0.0",
32
- "@cumulus/db": "12.0.0",
33
- "@cumulus/errors": "12.0.0",
34
- "@cumulus/es-client": "12.0.0",
35
- "@cumulus/types": "12.0.0",
31
+ "@cumulus/aws-client": "13.0.0",
32
+ "@cumulus/db": "13.0.0",
33
+ "@cumulus/errors": "13.0.0",
34
+ "@cumulus/es-client": "13.0.0",
35
+ "@cumulus/types": "13.0.0",
36
36
  "knex": "0.95.15",
37
37
  "uuid": "8.3.2"
38
38
  },
39
39
  "devDependencies": {
40
- "@cumulus/common": "12.0.0",
40
+ "@cumulus/common": "13.0.0",
41
41
  "@types/aws-sdk": "2.7.0",
42
42
  "@types/uuid": "^8.0.0"
43
43
  },
44
- "gitHead": "2d84198510f37d64444715736b8b08360f61c766"
44
+ "gitHead": "ba43ea3bfdfcacfff052770ed2fd357eb9dadb58"
45
45
  }
@@ -9,6 +9,7 @@ import {
9
9
  AsyncOperationPgModel,
10
10
  createRejectableTransaction,
11
11
  } from '@cumulus/db';
12
+ import Logger from '@cumulus/logger';
12
13
  import { ApiAsyncOperation, AsyncOperationType } from '@cumulus/types/api/async_operations';
13
14
  import { v4 as uuidv4 } from 'uuid';
14
15
  import type { AWSError } from 'aws-sdk/lib/error';
@@ -27,6 +28,8 @@ const {
27
28
  Search,
28
29
  } = require('@cumulus/es-client/search');
29
30
 
31
+ const logger = new Logger({ sender: '@cumulus/async-operation' });
32
+
30
33
  type StartEcsTaskReturnType = Promise<PromiseResult<ECS.RunTaskResponse, AWSError>>;
31
34
 
32
35
  export const getLambdaConfiguration = async (
@@ -198,6 +201,7 @@ export const createAsyncOperation = async (
198
201
  */
199
202
  export const startAsyncOperation = async (
200
203
  params: {
204
+ asyncOperationId?: string,
201
205
  asyncOperationTaskDefinition: string,
202
206
  cluster: string,
203
207
  description: string,
@@ -230,7 +234,7 @@ export const startAsyncOperation = async (
230
234
  throw new MissingRequiredArgument(`callerLambdaName must be specified to start new async operation, received: ${callerLambdaName}`);
231
235
  }
232
236
 
233
- const id = uuidv4();
237
+ const id = params.asyncOperationId ?? uuidv4();
234
238
  // Store the payload to S3
235
239
  const payloadBucket = systemBucket;
236
240
  const payloadKey = `${stackName}/async-operation-payloads/${id}.json`;
@@ -241,20 +245,50 @@ export const startAsyncOperation = async (
241
245
  Body: JSON.stringify(payload),
242
246
  });
243
247
 
244
- // Start the task in ECS
245
- const runTaskResponse = await startEcsTaskFunc({
246
- ...params,
247
- id,
248
- payloadBucket,
249
- payloadKey,
250
- });
248
+ logger.debug(`About to start AsyncOperation: ${id}`);
249
+ let runTaskResponse;
250
+ try {
251
+ runTaskResponse = await startEcsTaskFunc({
252
+ ...params,
253
+ id,
254
+ payloadBucket,
255
+ payloadKey,
256
+ });
251
257
 
252
- if (runTaskResponse?.failures && runTaskResponse.failures.length > 0) {
253
- throw new EcsStartTaskError(
254
- `Failed to start AsyncOperation: ${runTaskResponse.failures[0].reason}`
258
+ if (runTaskResponse?.failures && runTaskResponse.failures.length > 0) {
259
+ throw new EcsStartTaskError(
260
+ `Failed to start AsyncOperation: ${runTaskResponse.failures[0].reason}`
261
+ );
262
+ }
263
+ } catch (error) {
264
+ logger.error(`Failed to start AsyncOperation ${id}`, error);
265
+ const output = {
266
+ name: error.name,
267
+ message: error.message,
268
+ stack: error.stack,
269
+ };
270
+ await createAsyncOperation(
271
+ {
272
+ createObject: {
273
+ id,
274
+ status: 'RUNNER_FAILED',
275
+ output: JSON.stringify(output),
276
+ description,
277
+ operationType,
278
+ createdAt: Date.now(),
279
+ updatedAt: Date.now(),
280
+ },
281
+ stackName,
282
+ systemBucket,
283
+ dynamoTableName,
284
+ knexConfig,
285
+ },
286
+ AsyncOperation
255
287
  );
288
+ throw error;
256
289
  }
257
290
 
291
+ logger.debug(`About to create AsyncOperation record: ${id}`);
258
292
  return createAsyncOperation(
259
293
  {
260
294
  createObject: {
@@ -207,8 +207,61 @@ test.serial('The AsyncOperation start method starts an ECS task with the correct
207
207
  t.is(environmentOverrides.payloadUrl, `s3://${systemBucket}/${stackName}/async-operation-payloads/${id}.json`);
208
208
  });
209
209
 
210
- test.serial('The startAsyncOperation method throws error if it is unable to create an ECS task', async (t) => {
211
- const createSpy = sinon.spy((obj) => ({ id: obj.id }));
210
+ test.serial('The AsyncOperation start method starts an ECS task with the asyncOperationId passed in', async (t) => {
211
+ const createSpy = sinon.spy((obj) => obj);
212
+ const stubbedAsyncOperationsModel = class {
213
+ create = createSpy;
214
+ };
215
+
216
+ stubbedEcsRunTaskParams = {};
217
+ stubbedEcsRunTaskResult = {
218
+ tasks: [{ taskArn: randomString() }],
219
+ failures: [],
220
+ };
221
+
222
+ const asyncOperationId = uuidv4();
223
+ const asyncOperationTaskDefinition = randomString();
224
+ const cluster = randomString();
225
+ const callerLambdaName = randomString();
226
+ const lambdaName = randomString();
227
+ const payload = { x: randomString() };
228
+ const stackName = randomString();
229
+
230
+ const { id } = await startAsyncOperation({
231
+ asyncOperationId,
232
+ asyncOperationTaskDefinition,
233
+ cluster,
234
+ lambdaName,
235
+ callerLambdaName,
236
+ description: randomString(),
237
+ operationType: 'ES Index',
238
+ payload,
239
+ stackName,
240
+ dynamoTableName: dynamoTableName,
241
+ knexConfig: knexConfig,
242
+ systemBucket,
243
+ useLambdaEnvironmentVariables: true,
244
+ }, stubbedAsyncOperationsModel);
245
+
246
+ t.is(stubbedEcsRunTaskParams.cluster, cluster);
247
+ t.is(stubbedEcsRunTaskParams.taskDefinition, asyncOperationTaskDefinition);
248
+ t.is(stubbedEcsRunTaskParams.launchType, 'FARGATE');
249
+
250
+ const environmentOverrides = {};
251
+ stubbedEcsRunTaskParams.overrides.containerOverrides[0].environment.forEach((env) => {
252
+ environmentOverrides[env.name] = env.value;
253
+ });
254
+
255
+ t.is(id, asyncOperationId);
256
+ t.is(environmentOverrides.asyncOperationId, asyncOperationId);
257
+ t.is(environmentOverrides.asyncOperationsTable, dynamoTableName);
258
+ t.is(environmentOverrides.lambdaName, lambdaName);
259
+ t.is(environmentOverrides.payloadUrl, `s3://${systemBucket}/${stackName}/async-operation-payloads/${asyncOperationId}.json`);
260
+ });
261
+
262
+ test.serial('The startAsyncOperation method throws error and calls database model create method '
263
+ + 'when it is unable to create an ECS task', async (t) => {
264
+ const createSpy = sinon.spy((obj) => obj);
212
265
  const stubbedAsyncOperationsModel = class {
213
266
  create = createSpy;
214
267
  };
@@ -219,7 +272,8 @@ test.serial('The startAsyncOperation method throws error if it is unable to crea
219
272
  };
220
273
  const stackName = randomString();
221
274
 
222
- await t.throwsAsync(startAsyncOperation({
275
+ const asyncOperationParams = {
276
+ asyncOperationId: uuidv4(),
223
277
  asyncOperationTaskDefinition: randomString(),
224
278
  cluster: randomString(),
225
279
  callerLambdaName: randomString(),
@@ -231,10 +285,30 @@ test.serial('The startAsyncOperation method throws error if it is unable to crea
231
285
  dynamoTableName: dynamoTableName,
232
286
  knexConfig: knexConfig,
233
287
  systemBucket,
234
- }, stubbedAsyncOperationsModel), {
288
+ };
289
+ const expectedErrorThrown = {
235
290
  instanceOf: EcsStartTaskError,
236
291
  message: 'Failed to start AsyncOperation: out of cheese',
237
- });
292
+ };
293
+ await t.throwsAsync(
294
+ startAsyncOperation(asyncOperationParams, stubbedAsyncOperationsModel),
295
+ expectedErrorThrown
296
+ );
297
+
298
+ const spyCall = createSpy.getCall(0).args[0];
299
+
300
+ const expected = {
301
+ id: asyncOperationParams.asyncOperationId,
302
+ description: asyncOperationParams.description,
303
+ operationType: asyncOperationParams.operationType,
304
+ status: 'RUNNER_FAILED',
305
+ };
306
+
307
+ t.like(spyCall, expected);
308
+ t.deepEqual(omit(spyCall, ['createdAt', 'updatedAt', 'output']), expected);
309
+ t.is(spyCall.id, asyncOperationParams.asyncOperationId);
310
+ const output = JSON.parse(spyCall.output || {});
311
+ t.like(output, { name: 'EcsStartTaskError', message: expectedErrorThrown.message });
238
312
  });
239
313
 
240
314
  test('startAsyncOperation calls Dynamo model create method', async (t) => {