@squiz/job-runner-lib 2.3.0 → 2.4.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/domains/component/errors/JobError.spec.d.ts +1 -0
  3. package/lib/domains/job/ParsePartitionKey/ParsePartitionKey.d.ts +2 -2
  4. package/lib/domains/job/ParsePartitionKey/ParsePartitionKey.types.d.ts +5 -0
  5. package/lib/domains/job/S3Repository/JobDetailsRepository.d.ts +1 -2
  6. package/lib/domains/job/S3Repository/JobDetailsRepository.spec.d.ts +1 -1
  7. package/lib/domains/job/S3Repository/JobUploadsRepository.d.ts +16 -0
  8. package/lib/domains/job/{services/JobQueueService/JobQueueService.spec.d.ts → S3Repository/JobUploadsRepository.spec.d.ts} +1 -1
  9. package/lib/domains/job/S3Repository/S3RepositoryBase.d.ts +12 -5
  10. package/lib/domains/job/S3Repository/index.d.ts +2 -1
  11. package/lib/domains/job/constants/InjectTokens.d.ts +8 -0
  12. package/lib/domains/job/constants/JobExecutionStatus.constants.d.ts +2 -1
  13. package/lib/domains/job/constants/Repository.constants.d.ts +3 -3
  14. package/lib/domains/job/constants/RetryConfig.constants.d.ts +21 -0
  15. package/lib/domains/job/index.d.ts +6 -4
  16. package/lib/domains/job/manifest/JobManifestTypes.d.ts +7 -6
  17. package/lib/domains/job/model/Base/Base.d.ts +10 -5
  18. package/lib/domains/job/model/Job/Job.d.ts +9 -6
  19. package/lib/domains/job/model/JobContext/JobContext.d.ts +6 -5
  20. package/lib/domains/job/model/JobExecution/JobExecution.d.ts +64 -9
  21. package/lib/domains/job/model/LogPayload/LogPayload.d.ts +21 -12
  22. package/lib/domains/job/model/LogPayload/index.d.ts +1 -0
  23. package/lib/domains/job/model/LogPayload/mocks/LogPayload.mock.d.ts +6 -0
  24. package/lib/domains/job/repository/AbstractRepository.d.ts +20 -11
  25. package/lib/domains/job/repository/JobContextRepository/JobContextRepository.d.ts +6 -7
  26. package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.d.ts +7 -17
  27. package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.d.ts +1 -1
  28. package/lib/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.d.ts +2 -0
  29. package/lib/domains/job/repository/JobExecutionRepository/utils/index.d.ts +1 -0
  30. package/lib/domains/job/repository/JobManifestRepository/JobManifestRepository.d.ts +5 -6
  31. package/lib/domains/job/repository/JobRepository/JobRepository.d.ts +6 -7
  32. package/lib/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.d.ts +9 -0
  33. package/lib/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.spec.d.ts +1 -0
  34. package/lib/domains/job/repository/index.d.ts +1 -0
  35. package/lib/domains/job/services/JobContextService/JobContextService.d.ts +6 -3
  36. package/lib/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.d.ts +10 -6
  37. package/lib/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.defs.d.ts +1 -3
  38. package/lib/domains/job/services/JobManifestService/JobManifestService.d.ts +0 -1
  39. package/lib/domains/job/services/JobStartQueueService/JobStartQueueService.d.ts +17 -0
  40. package/lib/domains/job/services/JobStartQueueService/JobStartQueueService.defs.d.ts +25 -0
  41. package/lib/domains/job/services/JobUploadService/utils/manifestValidator.d.ts +1 -2
  42. package/lib/domains/job/services/index.d.ts +2 -1
  43. package/lib/domains/manifest/services/ManifestServiceForDev.d.ts +0 -3
  44. package/lib/index.d.ts +1 -0
  45. package/lib/index.js +13 -13
  46. package/lib/index.js.map +4 -4
  47. package/lib/jest-global-setup.d.ts +6 -1
  48. package/lib/mocks/mockIoc.d.ts +3 -3
  49. package/lib/utils/buildJobRunnerModule/buildJobRunnerModule.d.ts +2 -0
  50. package/lib/utils/buildJobRunnerModule/buildJobRunnerModule.types.d.ts +14 -0
  51. package/lib/utils/index.d.ts +2 -0
  52. package/package.json +12 -11
  53. package/src/domains/component/errors/JobError.spec.ts +30 -0
  54. package/src/domains/component/errors/JobError.ts +8 -1
  55. package/src/domains/job/ParsePartitionKey/ParsePartitionKey.spec.ts +17 -8
  56. package/src/domains/job/ParsePartitionKey/ParsePartitionKey.ts +13 -16
  57. package/src/domains/job/ParsePartitionKey/ParsePartitionKey.types.ts +6 -0
  58. package/src/domains/job/S3Repository/JobDetailsRepository.spec.ts +47 -31
  59. package/src/domains/job/S3Repository/JobDetailsRepository.ts +7 -8
  60. package/src/domains/job/S3Repository/JobUploadsRepository.spec.ts +66 -0
  61. package/src/domains/job/S3Repository/JobUploadsRepository.ts +35 -0
  62. package/src/domains/job/S3Repository/S3RepositoryBase.spec.ts +13 -19
  63. package/src/domains/job/S3Repository/S3RepositoryBase.ts +24 -9
  64. package/src/domains/job/S3Repository/index.ts +2 -1
  65. package/src/domains/job/constants/InjectTokens.ts +8 -0
  66. package/src/domains/job/constants/JobExecutionStatus.constants.ts +1 -0
  67. package/src/domains/job/constants/Repository.constants.ts +3 -3
  68. package/src/domains/job/constants/RetryConfig.constants.ts +29 -0
  69. package/src/domains/job/core/pagination/services/PaginationService.spec.ts +3 -5
  70. package/src/domains/job/index.ts +6 -4
  71. package/src/domains/job/manifest/JobManifestTypes.ts +8 -6
  72. package/src/domains/job/manifest/mocks/JobManifest.mock.ts +2 -2
  73. package/src/domains/job/manifest/mocks/JobManifestDto.mock.ts +3 -2
  74. package/src/domains/job/manifest/mocks/JobManifestRecord.mock.ts +4 -3
  75. package/src/domains/job/model/Base/Base.ts +11 -5
  76. package/src/domains/job/model/Job/Job.ts +11 -6
  77. package/src/domains/job/model/Job/mocks/Job.mock.ts +2 -2
  78. package/src/domains/job/model/Job/mocks/JobDto.mock.ts +3 -2
  79. package/src/domains/job/model/Job/mocks/JobRecord.mock.ts +3 -2
  80. package/src/domains/job/model/JobContext/JobContext.ts +7 -5
  81. package/src/domains/job/model/JobContext/mocks/JobContext.mock.ts +2 -2
  82. package/src/domains/job/model/JobContext/mocks/JobContextDto.mock.ts +3 -2
  83. package/src/domains/job/model/JobContext/mocks/JobContextRecord.mock.ts +3 -2
  84. package/src/domains/job/model/JobExecution/JobExecution.ts +74 -9
  85. package/src/domains/job/model/JobExecution/mocks/JobExecution.mock.ts +7 -4
  86. package/src/domains/job/model/JobExecution/mocks/JobExecutionDto.mock.ts +32 -16
  87. package/src/domains/job/model/JobExecution/mocks/JobExecutionRecord.mock.ts +10 -4
  88. package/src/domains/job/model/LogPayload/LogPayload.ts +32 -11
  89. package/src/domains/job/model/LogPayload/index.ts +1 -0
  90. package/src/domains/job/model/LogPayload/mocks/LogPayload.mock.ts +21 -0
  91. package/src/domains/job/repository/AbstractRepository.ts +91 -59
  92. package/src/domains/job/repository/JobContextRepository/JobContextRepository.spec.ts +39 -49
  93. package/src/domains/job/repository/JobContextRepository/JobContextRepository.ts +33 -21
  94. package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.ts +343 -169
  95. package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.ts +112 -141
  96. package/src/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.ts +22 -0
  97. package/src/domains/job/repository/JobExecutionRepository/utils/index.ts +1 -0
  98. package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.spec.ts +23 -38
  99. package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.ts +29 -36
  100. package/src/domains/job/repository/JobRepository/JobRepository.spec.ts +72 -80
  101. package/src/domains/job/repository/JobRepository/JobRepository.ts +66 -70
  102. package/src/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.spec.ts +60 -0
  103. package/src/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.ts +42 -0
  104. package/src/domains/job/repository/index.ts +1 -0
  105. package/src/domains/job/services/JobContextService/JobContextService.spec.ts +73 -39
  106. package/src/domains/job/services/JobContextService/JobContextService.ts +37 -6
  107. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.defs.ts +1 -3
  108. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.spec.ts +113 -31
  109. package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.ts +101 -49
  110. package/src/domains/job/services/JobManifestService/JobManifestService.spec.ts +4 -41
  111. package/src/domains/job/services/JobManifestService/JobManifestService.ts +2 -8
  112. package/src/domains/job/services/JobStartQueueService/JobStartQueueService.defs.ts +27 -0
  113. package/src/domains/job/services/JobStartQueueService/JobStartQueueService.ts +42 -0
  114. package/src/domains/job/services/JobUploadService/JobUploadService.spec.ts +1 -1
  115. package/src/domains/job/services/JobUploadService/JobUploadService.ts +1 -1
  116. package/src/domains/job/services/JobUploadService/utils/manifestValidator.spec.ts +12 -25
  117. package/src/domains/job/services/JobUploadService/utils/manifestValidator.ts +2 -7
  118. package/src/domains/job/services/index.ts +2 -1
  119. package/src/domains/job/utils/SlackClient/SlackClient.spec.ts +0 -2
  120. package/src/domains/manifest/services/ManifestServiceForDev.spec.ts +1 -2
  121. package/src/domains/manifest/services/ManifestServiceForDev.ts +0 -4
  122. package/src/index.ts +1 -0
  123. package/src/jest-global-setup.ts +8 -4
  124. package/src/mocks/mockIoc.ts +18 -43
  125. package/src/utils/buildJobRunnerModule/buildJobRunnerModule.ts +81 -0
  126. package/src/utils/buildJobRunnerModule/buildJobRunnerModule.types.ts +13 -0
  127. package/src/utils/index.ts +2 -0
  128. package/lib/domains/job/S3Repository/S3Repository.d.ts +0 -15
  129. package/lib/domains/job/S3Repository/S3Repository.spec.d.ts +0 -5
  130. package/lib/domains/job/services/JobQueueService/JobQueueService.d.ts +0 -9
  131. package/lib/domains/job/services/JobQueueService/JobQueueService.defs.d.ts +0 -9
  132. package/src/custom-types.d.ts +0 -7
  133. package/src/domains/job/S3Repository/S3Repository.spec.ts +0 -50
  134. package/src/domains/job/S3Repository/S3Repository.ts +0 -27
  135. package/src/domains/job/services/JobQueueService/JobQueueService.defs.ts +0 -10
  136. package/src/domains/job/services/JobQueueService/JobQueueService.spec.ts +0 -190
  137. package/src/domains/job/services/JobQueueService/JobQueueService.ts +0 -60
@@ -0,0 +1,42 @@
1
+ import { AttributeValue, DynamoDBClient, QueryCommand, QueryCommandOutput } from '@aws-sdk/client-dynamodb';
2
+ import { JobExecutionStatus } from '../../constants/JobExecutionStatus.constants';
3
+ import { JobExecutionDto, JobExecutionsByTenant } from '../../model/JobExecution';
4
+ import { parsePartitionKey } from '../../ParsePartitionKey';
5
+ import { convertDynamoDbItemToJobExecution } from '../JobExecutionRepository/utils';
6
+ import { ENTITY_TYPES } from '../../constants/Repository.constants';
7
+
8
+ export class MultiTenantJobExecutionRepository {
9
+ public constructor(
10
+ private readonly dynamoDbClient: DynamoDBClient,
11
+ private readonly tableName: string,
12
+ ) {}
13
+
14
+ public async findByStatus(status: JobExecutionStatus): Promise<JobExecutionsByTenant> {
15
+ const result: JobExecutionsByTenant = {};
16
+ let startKey: Record<string, AttributeValue> | undefined = undefined;
17
+
18
+ do {
19
+ const res: QueryCommandOutput = await this.dynamoDbClient.send(
20
+ new QueryCommand({
21
+ KeyConditionExpression: 'gsi1pk = :gsi1pk',
22
+ ExpressionAttributeValues: {
23
+ ':gsi1pk': { S: `${ENTITY_TYPES.jobExecution}~${status}` },
24
+ },
25
+ TableName: this.tableName,
26
+ IndexName: 'gsi1',
27
+ ExclusiveStartKey: startKey,
28
+ }),
29
+ );
30
+
31
+ res.Items?.forEach((item) => {
32
+ const { tenant } = parsePartitionKey(item.pk.S || '');
33
+ result[tenant] ||= [];
34
+ result[tenant].push(convertDynamoDbItemToJobExecution(item as JobExecutionDto));
35
+ });
36
+
37
+ startKey = res.LastEvaluatedKey;
38
+ } while (startKey);
39
+
40
+ return result;
41
+ }
42
+ }
@@ -2,4 +2,5 @@ export * from './JobContextRepository/JobContextRepository';
2
2
  export * from './JobExecutionRepository/JobExecutionRepository';
3
3
  export * from './JobManifestRepository/JobManifestRepository';
4
4
  export * from './JobRepository/JobRepository';
5
+ export * from './MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository';
5
6
  export * from './AbstractRepository';
@@ -6,7 +6,6 @@ import {
6
6
  AttributeValue,
7
7
  DeleteItemCommandOutput,
8
8
  DynamoDBClient,
9
- DynamoDBClientConfig,
10
9
  GetItemCommandOutput,
11
10
  PutItemCommandOutput,
12
11
  ScanCommandOutput,
@@ -15,7 +14,7 @@ import {
15
14
  import * as PaginationUtils from '../../core/pagination/utils/PaginationUtils';
16
15
  import { faker } from '@faker-js/faker/locale/en';
17
16
  import { JobContextService } from './JobContextService';
18
- import { PAGINATION_DIRECTION_TYPE_AFTER, PaginationService } from '../../core/pagination';
17
+ import { PAGINATION_DIRECTION_TYPE_AFTER } from '../../core/pagination';
19
18
  import { JobContextRepository } from '../../repository';
20
19
  import {
21
20
  createMockCreateJobContextRequest,
@@ -31,41 +30,45 @@ import {
31
30
  createMockClientPutResponse,
32
31
  createMockClientScanResponse,
33
32
  } from '../../core/dynamoClient.mock';
34
- import { PARTITION_KEYS } from '../../constants/Repository.constants';
33
+ import { ENTITY_TYPES } from '../../constants/Repository.constants';
34
+ import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc';
35
+ import { testLogger } from '../../../../jest-global-setup';
35
36
 
36
37
  // Mock DynamoDB Client
37
- const mockOptions = {} as DynamoDBClientConfig;
38
- const mockClient = new DynamoDBClient(mockOptions);
38
+ const mockSend = jest.spyOn(DynamoDBClient.prototype, 'send');
39
39
 
40
- const mockSend = jest.fn();
41
- mockClient.send = mockSend;
40
+ initIocContainer(testLogger);
42
41
 
43
- let jobContextService: JobContextService;
44
- let paginationService: PaginationService<any>;
45
42
  describe('JobContextService', (): void => {
43
+ let jobContextService: JobContextService;
44
+
46
45
  beforeAll((): void => {
47
- const jobContextRepository = new JobContextRepository(mockClient);
48
- paginationService = new PaginationService();
49
- jobContextService = new JobContextService(jobContextRepository, paginationService);
46
+ jobContextService = iocContainer.get(JobContextService);
50
47
  });
51
48
 
52
49
  describe(`createJobContext`, (): void => {
53
- beforeEach(() => {
54
- jest.resetAllMocks();
55
- });
56
-
57
50
  it(`should return a response for createJobContext`, async (): Promise<void> => {
58
- const newJobContextToCreate = createMockCreateJobContextRequest();
51
+ const newJobContextToCreate = createMockCreateJobContextRequest({
52
+ environment: {
53
+ secret1: faker.string.uuid(),
54
+ secret2: faker.string.uuid(),
55
+ },
56
+ });
59
57
  const environmentMap: Record<string, AttributeValue> = {};
60
58
  for (const key of Object.keys(newJobContextToCreate.environment)) {
61
59
  environmentMap[key] = { S: newJobContextToCreate.environment[key] };
62
60
  }
63
61
  const mockResponse = createMockClientPutResponse({
64
62
  Attributes: {
65
- pk: { S: PARTITION_KEYS.context },
63
+ pk: { S: ENTITY_TYPES.context },
66
64
  sk: { S: newJobContextToCreate.contextName },
67
65
  name: { S: newJobContextToCreate.contextName },
68
- version: { M: environmentMap },
66
+ version: {
67
+ M: {
68
+ secret1: { S: newJobContextToCreate.environment.secret1 },
69
+ secret2: { S: newJobContextToCreate.environment.secret2 },
70
+ },
71
+ },
69
72
  },
70
73
  });
71
74
 
@@ -75,16 +78,27 @@ describe('JobContextService', (): void => {
75
78
  expect(mockSend).toBeCalledTimes(1);
76
79
  expect(response).toStrictEqual({
77
80
  ...newJobContextToCreate,
78
- type: PARTITION_KEYS.context,
81
+ type: ENTITY_TYPES.context,
82
+ });
83
+ expect(testLogger.audit).toHaveBeenCalledWith(`Created job context ${newJobContextToCreate.contextName}`, {
84
+ context: expect.any(String),
85
+ processable: true,
86
+ remote: true,
87
+ details: {
88
+ context: {
89
+ contextName: newJobContextToCreate.contextName,
90
+ description: newJobContextToCreate.description,
91
+ environment: {
92
+ secret1: '*redacted*',
93
+ secret2: '*redacted*',
94
+ },
95
+ },
96
+ },
79
97
  });
80
98
  });
81
99
  });
82
100
 
83
101
  describe(`deleteJobContext`, (): void => {
84
- beforeEach(() => {
85
- jest.resetAllMocks();
86
- });
87
-
88
102
  it(`should return an empty response for job context deletion`, async (): Promise<void> => {
89
103
  mockSend.mockImplementationOnce((): DeleteItemCommandOutput => {
90
104
  return {
@@ -92,17 +106,32 @@ describe('JobContextService', (): void => {
92
106
  };
93
107
  });
94
108
 
95
- const response = await jobContextService.deleteJobContext(`name`);
109
+ const context = createMockJobContext({
110
+ environment: {
111
+ secret1: faker.string.uuid(),
112
+ },
113
+ });
114
+ const response = await jobContextService.deleteJobContext(context);
96
115
  expect(response).toBe(undefined);
97
116
  expect(mockSend).toBeCalledTimes(1);
117
+ expect(testLogger.audit).toHaveBeenCalledWith(`Deleted job context ${context.contextName}`, {
118
+ context: expect.any(String),
119
+ processable: true,
120
+ remote: true,
121
+ details: {
122
+ context: {
123
+ contextName: context.contextName,
124
+ description: context.description,
125
+ environment: {
126
+ secret1: '*redacted*',
127
+ },
128
+ },
129
+ },
130
+ });
98
131
  });
99
132
  });
100
133
 
101
134
  describe(`findJobContext`, (): void => {
102
- beforeEach(() => {
103
- jest.resetAllMocks();
104
- });
105
-
106
135
  it(`should return job context by name`, async (): Promise<void> => {
107
136
  const mockJobContextRecord = createMockJobContextDto({
108
137
  environment: { key: 'value' },
@@ -140,10 +169,6 @@ describe('JobContextService', (): void => {
140
169
  });
141
170
 
142
171
  describe(`listAllJobContexts`, (): void => {
143
- beforeEach(() => {
144
- jest.resetAllMocks();
145
- });
146
-
147
172
  it(`should return an empty list`, async (): Promise<void> => {
148
173
  mockSend.mockImplementationOnce((): ScanCommandOutput => createMockClientScanResponse());
149
174
 
@@ -202,8 +227,6 @@ describe('JobContextService', (): void => {
202
227
  let getContextsSpy: any;
203
228
 
204
229
  beforeEach(() => {
205
- jest.resetAllMocks();
206
-
207
230
  mockJobContexts = createMockJobContexts();
208
231
 
209
232
  mockPaginationInfo = {
@@ -235,10 +258,6 @@ describe('JobContextService', (): void => {
235
258
  });
236
259
 
237
260
  describe(`updateJobContext`, (): void => {
238
- beforeEach(() => {
239
- jest.resetAllMocks();
240
- });
241
-
242
261
  it(`should update a job context record`, async (): Promise<void> => {
243
262
  const jobContextToUpdate = createMockJobContext();
244
263
  const updatedJobContext: JobContext = {
@@ -267,6 +286,21 @@ describe('JobContextService', (): void => {
267
286
  );
268
287
  expect(response).toStrictEqual(updatedJobContext);
269
288
  expect(mockSend).toBeCalledTimes(1);
289
+ expect(testLogger.audit).toHaveBeenCalledWith(`Updated job context ${updatedJobContext.contextName}`, {
290
+ context: expect.any(String),
291
+ processable: true,
292
+ remote: true,
293
+ details: {
294
+ context: {
295
+ contextName: updatedJobContext.contextName,
296
+ description: updatedJobContext.description,
297
+ environment: {
298
+ name: '*redacted*',
299
+ age: '*redacted*',
300
+ },
301
+ },
302
+ },
303
+ });
270
304
  });
271
305
  });
272
306
  });
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { inject } from 'inversify';
7
+ import { produce } from 'immer';
7
8
  import { JobContextRepository } from '../../repository';
8
9
  import { provideSingleton } from '../../singletonProvider';
9
10
  import {
@@ -16,20 +17,27 @@ import {
16
17
  PaginationService,
17
18
  } from '../../core/pagination';
18
19
  import { CreateJobContextRequest, JobContext, JobContextDto, UpdateJobContextRequest } from '../../model/JobContext';
20
+ import { CreateLog, Logger } from '@squiz/optimization-logger';
21
+ import { InjectTokens } from '../../constants/InjectTokens';
19
22
 
20
23
  @provideSingleton(JobContextService)
21
24
  export class JobContextService {
22
25
  constructor(
23
26
  @inject(JobContextRepository) protected jobContextRepository: JobContextRepository,
24
27
  @inject(PaginationService) protected paginationService: PaginationService<JobContext>,
28
+ @inject(InjectTokens.Logger) protected logger: Logger,
25
29
  ) {}
26
30
 
27
31
  public async createJobContext(input: CreateJobContextRequest): Promise<JobContext> {
28
- return this.jobContextRepository.create(input);
32
+ const context = await this.jobContextRepository.create(input);
33
+ this.logContextAction(`Created job context ${context.contextName}`, context);
34
+ return context;
29
35
  }
30
36
 
31
- public async updateJobContext(name: string, input: UpdateJobContextRequest): Promise<JobContext | null> {
32
- return this.jobContextRepository.update(name, input);
37
+ public async updateJobContext(contextName: string, input: UpdateJobContextRequest): Promise<JobContext | null> {
38
+ const context = await this.jobContextRepository.update({ contextName }, input);
39
+ this.logContextAction(`Updated job context ${context.contextName}`, context);
40
+ return context;
33
41
  }
34
42
 
35
43
  public async listAllJobContexts(): Promise<Array<JobContext>> {
@@ -68,10 +76,33 @@ export class JobContextService {
68
76
  }
69
77
 
70
78
  public async findJobContext(contextName: string): Promise<JobContext | null> {
71
- return this.jobContextRepository.findOne(contextName);
79
+ return this.jobContextRepository.findOne({ contextName });
72
80
  }
73
81
 
74
- public async deleteJobContext(contextName: string): Promise<void> {
75
- return await this.jobContextRepository.delete(contextName);
82
+ public async deleteJobContext(context: JobContext): Promise<void> {
83
+ await this.jobContextRepository.delete({ contextName: context.contextName });
84
+ this.logContextAction(`Deleted job context ${context.contextName}`, context);
85
+ }
86
+
87
+ private logContextAction(message: string, context: JobContext) {
88
+ const { type: _, ...maskedContext } = produce(context, (draft) => {
89
+ for (const key in draft.environment) {
90
+ draft.environment[key] = '*redacted*';
91
+ }
92
+
93
+ return draft;
94
+ });
95
+
96
+ const log = CreateLog.create()
97
+ .attachMetadata({
98
+ details: {
99
+ context: maskedContext,
100
+ },
101
+ })
102
+ .processable()
103
+ .remote()
104
+ .create();
105
+
106
+ this.logger.audit(...log(message));
76
107
  }
77
108
  }
@@ -3,9 +3,7 @@ import { JobExecution } from '../../model/JobExecution';
3
3
  /**
4
4
  * The required parameters for retrieving the number of running ECS tasks
5
5
  */
6
- export interface ListRunningTasksParams {
7
- clusterArn: string;
8
- runtimeTaskFamily: string;
6
+ export interface HasReachedMaxTenantConcurrencyParams {
9
7
  maxTenantConcurrentTasks: number;
10
8
  }
11
9
 
@@ -1,22 +1,25 @@
1
1
  import 'aws-sdk-client-mock-jest';
2
2
  import { AwsClientStub, mockClient } from 'aws-sdk-client-mock';
3
- import { ECSClient, ListTasksCommand, RunTaskCommand } from '@aws-sdk/client-ecs';
3
+ import { ConflictException, ResourceNotFoundException, ECSClient, RunTaskCommand } from '@aws-sdk/client-ecs';
4
4
  import { faker } from '@faker-js/faker';
5
5
  import { JobExecutionTaskService } from './JobExecutionTaskService';
6
6
  import { JobManifestService } from '../JobManifestService/JobManifestService';
7
7
  import { JobContextService } from '../JobContextService/JobContextService';
8
- import { JobDetailsRepository, S3Repository } from '../../S3Repository';
8
+ import { JobDetailsRepository, JobUploadsRepository } from '../../S3Repository';
9
9
  import { createMockJobExecution } from '../../model/JobExecution';
10
10
  import { createMockJobContext } from '../../model/JobContext';
11
11
  import { createMockJobManifest } from '../../manifest';
12
12
  import { RunJobTaskParams } from './JobExecutionTaskService.defs';
13
+ import { JobExecutionRepository } from '../../repository';
14
+ import { JOB_EXECUTION_STATUS } from '../../constants/JobExecutionStatus.constants';
13
15
 
14
16
  describe('JobExecutionTaskService', () => {
15
17
  let mockEcsClient: AwsClientStub<ECSClient>;
16
18
  let mockJobManifestService: jest.Mocked<JobManifestService>;
17
19
  let mockJobContextService: jest.Mocked<JobContextService>;
20
+ let mockJobExecutionRepository: jest.Mocked<JobExecutionRepository>;
18
21
  let mockJobDetailsRepository: jest.Mocked<JobDetailsRepository>;
19
- let mockJobUploadRepository: jest.Mocked<S3Repository>;
22
+ let mockJobUploadRepository: jest.Mocked<JobUploadsRepository>;
20
23
  let jobExecutionTaskService: JobExecutionTaskService;
21
24
 
22
25
  const jobDetailsSignedUrl = faker.internet.url();
@@ -33,8 +36,6 @@ describe('JobExecutionTaskService', () => {
33
36
  };
34
37
 
35
38
  beforeEach(() => {
36
- jest.resetAllMocks();
37
-
38
39
  mockEcsClient = mockClient(ECSClient);
39
40
  mockJobManifestService = {
40
41
  findJobManifest: jest.fn(),
@@ -42,59 +43,60 @@ describe('JobExecutionTaskService', () => {
42
43
  mockJobContextService = {
43
44
  findJobContext: jest.fn(),
44
45
  } as unknown as jest.Mocked<JobContextService>;
46
+ mockJobExecutionRepository = {
47
+ list: jest.fn(),
48
+ } as unknown as jest.Mocked<JobExecutionRepository>;
45
49
  mockJobDetailsRepository = {
46
50
  createJobDetailsFile: jest.fn(),
47
51
  getS3SignedUrl: jest.fn().mockResolvedValue(jobDetailsSignedUrl),
48
52
  } as unknown as jest.Mocked<JobDetailsRepository>;
49
53
  mockJobUploadRepository = {
50
54
  getS3SignedUrl: jest.fn().mockResolvedValue(jobCodeArtifactSignedUrl),
51
- } as unknown as jest.Mocked<S3Repository>;
55
+ } as unknown as jest.Mocked<JobUploadsRepository>;
52
56
 
53
57
  jobExecutionTaskService = new JobExecutionTaskService(
54
58
  new ECSClient({}),
55
59
  mockJobManifestService,
56
60
  mockJobContextService,
61
+ mockJobExecutionRepository,
57
62
  mockJobDetailsRepository,
58
63
  mockJobUploadRepository,
59
64
  );
60
65
  });
61
66
 
62
- describe(`getTenantRunningTasks`, () => {
63
- it('should 0 if no tasks were found', async (): Promise<void> => {
64
- mockEcsClient.on(ListTasksCommand).resolves({});
65
- const response = await (jobExecutionTaskService as any).getTenantRunningTasks({
66
- clusterArn: `clusterArn`,
67
- runtimeTaskFamily: 'some_family',
68
- maxTenantConcurrentTasks: 3,
69
- });
70
-
71
- expect(response).toBe(0);
72
- });
73
- });
74
-
75
67
  describe(`hasReachedMaxTenantConcurrency`, () => {
76
68
  it('should return false if number of running jobs is less than max', async (): Promise<void> => {
77
- mockEcsClient.on(ListTasksCommand).resolves({ taskArns: ['12345', '67890'] });
69
+ const maxTenantConcurrentTasks = 3;
70
+
71
+ mockJobExecutionRepository.list.mockResolvedValue([createMockJobExecution(), createMockJobExecution()]);
78
72
 
79
73
  const response = await jobExecutionTaskService.hasReachedMaxTenantConcurrency({
80
- clusterArn: `clusterArn`,
81
- runtimeTaskFamily: 'some_family',
82
- maxTenantConcurrentTasks: 3,
74
+ maxTenantConcurrentTasks,
83
75
  });
84
76
 
85
77
  expect(response).toBe(false);
78
+ expect(mockJobExecutionRepository.list).toHaveBeenCalledWith({
79
+ limit: maxTenantConcurrentTasks,
80
+ consistentRead: true,
81
+ filters: { status: JOB_EXECUTION_STATUS.running },
82
+ });
86
83
  });
87
84
 
88
85
  it('should return true if number of running jobs is more than max', async (): Promise<void> => {
89
- mockEcsClient.on(ListTasksCommand).resolves({ taskArns: ['12345', '67890'] });
86
+ const maxTenantConcurrentTasks = 1;
87
+
88
+ mockJobExecutionRepository.list.mockResolvedValue([createMockJobExecution(), createMockJobExecution()]);
90
89
 
91
90
  const response = await jobExecutionTaskService.hasReachedMaxTenantConcurrency({
92
- clusterArn: `clusterArn`,
93
- runtimeTaskFamily: 'some_family',
94
- maxTenantConcurrentTasks: 1,
91
+ maxTenantConcurrentTasks,
95
92
  });
96
93
 
97
94
  expect(response).toBe(true);
95
+ expect(mockJobExecutionRepository.list).toHaveBeenCalledWith({
96
+ limit: maxTenantConcurrentTasks,
97
+ consistentRead: true,
98
+ filters: { status: JOB_EXECUTION_STATUS.running },
99
+ });
98
100
  });
99
101
  });
100
102
 
@@ -105,7 +107,7 @@ describe('JobExecutionTaskService', () => {
105
107
  const jobContext = createMockJobContext();
106
108
  const jobManifest = createMockJobManifest();
107
109
  const jobExecution = createMockJobExecution({
108
- id: `${new Date().toISOString()}-jobExecutionId`,
110
+ id: `20250917T005949395Z-9602417782`,
109
111
  jobName: 'jobName',
110
112
  version: 'jobVersion',
111
113
  });
@@ -175,15 +177,17 @@ describe('JobExecutionTaskService', () => {
175
177
  { key: 'cmp:tenant', value: 'some_tenant' },
176
178
  ],
177
179
  taskDefinition: defaultRunJobTaskParams.taskDefinitionArn,
180
+ // sha1 of execution job name/id + tenant + retryCount.
181
+ clientToken: '4725755e08138edde031a177c36a269ecdbed5a3',
178
182
  });
179
183
  expect(mockJobDetailsRepository.createJobDetailsFile).toHaveBeenCalledWith(
180
- `${defaultRunJobTaskParams.tenantId}/${jobExecution.id}`,
184
+ defaultRunJobTaskParams.tenantId,
181
185
  jobContext,
182
186
  jobManifest,
183
187
  jobExecution,
184
188
  );
185
189
  expect(mockJobUploadRepository.getS3SignedUrl).toHaveBeenCalledWith(
186
- `${jobExecution.jobName}/${jobExecution.version}/export.zip`,
190
+ `${defaultRunJobTaskParams.tenantId}/${jobExecution.jobName}/${jobExecution.version}/export.zip`,
187
191
  );
188
192
  });
189
193
 
@@ -201,11 +205,89 @@ describe('JobExecutionTaskService', () => {
201
205
  await jobExecutionTaskService.runJobExecutionTask(defaultRunJobTaskParams);
202
206
 
203
207
  expect(mockJobDetailsRepository.createJobDetailsFile).toHaveBeenCalledWith(
204
- `${defaultRunJobTaskParams.tenantId}/${defaultRunJobTaskParams.jobExecution.id}`,
208
+ defaultRunJobTaskParams.tenantId,
205
209
  null,
206
210
  null,
207
211
  defaultRunJobTaskParams.jobExecution,
208
212
  );
209
213
  });
214
+
215
+ it('should handle ConflictException with a previously started task arn', async () => {
216
+ const mockArn = `mock-arn`;
217
+ const jobContext = createMockJobContext();
218
+ const jobManifest = createMockJobManifest();
219
+ const jobExecution = createMockJobExecution({
220
+ id: `20250917T005949395Z-9602417782`,
221
+ jobName: 'jobName',
222
+ version: 'jobVersion',
223
+ });
224
+
225
+ mockJobContextService.findJobContext.mockResolvedValue(jobContext);
226
+ mockJobManifestService.findJobManifest.mockResolvedValue(jobManifest);
227
+ mockEcsClient.on(RunTaskCommand).rejects(
228
+ new ConflictException({
229
+ message: 'The RunTask request could not be processed due to conflicts.',
230
+ resourceIds: [mockArn],
231
+ $metadata: {},
232
+ }),
233
+ );
234
+
235
+ const response = await jobExecutionTaskService.runJobExecutionTask({
236
+ ...defaultRunJobTaskParams,
237
+ jobExecution: jobExecution,
238
+ });
239
+
240
+ expect(response).toBe(mockArn);
241
+ });
242
+
243
+ it('should handle ConflictException with no previously started task arn', async () => {
244
+ const jobContext = createMockJobContext();
245
+ const jobManifest = createMockJobManifest();
246
+ const jobExecution = createMockJobExecution({
247
+ id: `20250917T005949395Z-9602417782`,
248
+ jobName: 'jobName',
249
+ version: 'jobVersion',
250
+ });
251
+
252
+ mockJobContextService.findJobContext.mockResolvedValue(jobContext);
253
+ mockJobManifestService.findJobManifest.mockResolvedValue(jobManifest);
254
+ mockEcsClient.on(RunTaskCommand).rejects(
255
+ new ConflictException({
256
+ message: 'The RunTask request could not be processed due to conflicts.',
257
+ resourceIds: [],
258
+ $metadata: {},
259
+ }),
260
+ );
261
+
262
+ const response = await jobExecutionTaskService.runJobExecutionTask({
263
+ ...defaultRunJobTaskParams,
264
+ jobExecution: jobExecution,
265
+ });
266
+
267
+ // A ConflictException may be thrown without returning the ARN of the task.
268
+ expect(response).toBe(undefined);
269
+ });
270
+
271
+ it('should re-throw unhandled errors when starting a task', async () => {
272
+ const expectedError = new ResourceNotFoundException({ message: faker.lorem.words(), $metadata: {} });
273
+ const jobContext = createMockJobContext();
274
+ const jobManifest = createMockJobManifest();
275
+ const jobExecution = createMockJobExecution({
276
+ id: `20250917T005949395Z-9602417782`,
277
+ jobName: 'jobName',
278
+ version: 'jobVersion',
279
+ });
280
+
281
+ mockJobContextService.findJobContext.mockResolvedValue(jobContext);
282
+ mockJobManifestService.findJobManifest.mockResolvedValue(jobManifest);
283
+ mockEcsClient.on(RunTaskCommand).rejects(expectedError);
284
+
285
+ await expect(
286
+ jobExecutionTaskService.runJobExecutionTask({
287
+ ...defaultRunJobTaskParams,
288
+ jobExecution: jobExecution,
289
+ }),
290
+ ).rejects.toThrow(expectedError);
291
+ });
210
292
  });
211
293
  });