@squiz/job-runner-lib 2.2.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.
- package/CHANGELOG.md +12 -0
- package/lib/domains/component/errors/JobError.spec.d.ts +1 -0
- package/lib/domains/job/ParsePartitionKey/ParsePartitionKey.d.ts +2 -2
- package/lib/domains/job/ParsePartitionKey/ParsePartitionKey.types.d.ts +5 -0
- package/lib/domains/job/S3Repository/JobDetailsRepository.d.ts +1 -2
- package/lib/domains/job/S3Repository/JobDetailsRepository.spec.d.ts +1 -1
- package/lib/domains/job/S3Repository/JobUploadsRepository.d.ts +16 -0
- package/lib/domains/job/{services/JobQueueService/JobQueueService.spec.d.ts → S3Repository/JobUploadsRepository.spec.d.ts} +1 -1
- package/lib/domains/job/S3Repository/S3RepositoryBase.d.ts +12 -5
- package/lib/domains/job/S3Repository/index.d.ts +2 -1
- package/lib/domains/job/constants/InjectTokens.d.ts +8 -0
- package/lib/domains/job/constants/JobExecutionStatus.constants.d.ts +2 -1
- package/lib/domains/job/constants/Repository.constants.d.ts +3 -3
- package/lib/domains/job/constants/RetryConfig.constants.d.ts +21 -0
- package/lib/domains/job/index.d.ts +6 -4
- package/lib/domains/job/manifest/JobManifestTypes.d.ts +7 -6
- package/lib/domains/job/model/Base/Base.d.ts +10 -5
- package/lib/domains/job/model/Job/Job.d.ts +9 -6
- package/lib/domains/job/model/JobContext/JobContext.d.ts +6 -5
- package/lib/domains/job/model/JobExecution/JobExecution.d.ts +64 -9
- package/lib/domains/job/model/LogPayload/LogPayload.d.ts +21 -12
- package/lib/domains/job/model/LogPayload/index.d.ts +1 -0
- package/lib/domains/job/model/LogPayload/mocks/LogPayload.mock.d.ts +6 -0
- package/lib/domains/job/repository/AbstractRepository.d.ts +20 -11
- package/lib/domains/job/repository/JobContextRepository/JobContextRepository.d.ts +6 -7
- package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.d.ts +7 -17
- package/lib/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.d.ts +1 -1
- package/lib/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.d.ts +2 -0
- package/lib/domains/job/repository/JobExecutionRepository/utils/index.d.ts +1 -0
- package/lib/domains/job/repository/JobManifestRepository/JobManifestRepository.d.ts +5 -6
- package/lib/domains/job/repository/JobRepository/JobRepository.d.ts +6 -7
- package/lib/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.d.ts +9 -0
- package/lib/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.spec.d.ts +1 -0
- package/lib/domains/job/repository/index.d.ts +1 -0
- package/lib/domains/job/services/JobContextService/JobContextService.d.ts +6 -3
- package/lib/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.d.ts +10 -6
- package/lib/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.defs.d.ts +1 -3
- package/lib/domains/job/services/JobManifestService/JobManifestService.d.ts +0 -1
- package/lib/domains/job/services/JobStartQueueService/JobStartQueueService.d.ts +17 -0
- package/lib/domains/job/services/JobStartQueueService/JobStartQueueService.defs.d.ts +25 -0
- package/lib/domains/job/services/JobUploadService/utils/manifestValidator.d.ts +1 -2
- package/lib/domains/job/services/index.d.ts +2 -1
- package/lib/domains/manifest/services/ManifestServiceForDev.d.ts +0 -3
- package/lib/index.d.ts +1 -0
- package/lib/index.js +13 -13
- package/lib/index.js.map +4 -4
- package/lib/jest-global-setup.d.ts +6 -1
- package/lib/mocks/mockIoc.d.ts +3 -3
- package/lib/utils/buildJobRunnerModule/buildJobRunnerModule.d.ts +2 -0
- package/lib/utils/buildJobRunnerModule/buildJobRunnerModule.types.d.ts +14 -0
- package/lib/utils/index.d.ts +2 -0
- package/package.json +12 -11
- package/src/domains/component/errors/JobError.spec.ts +30 -0
- package/src/domains/component/errors/JobError.ts +8 -1
- package/src/domains/job/ParsePartitionKey/ParsePartitionKey.spec.ts +17 -8
- package/src/domains/job/ParsePartitionKey/ParsePartitionKey.ts +13 -16
- package/src/domains/job/ParsePartitionKey/ParsePartitionKey.types.ts +6 -0
- package/src/domains/job/S3Repository/JobDetailsRepository.spec.ts +47 -31
- package/src/domains/job/S3Repository/JobDetailsRepository.ts +7 -8
- package/src/domains/job/S3Repository/JobUploadsRepository.spec.ts +66 -0
- package/src/domains/job/S3Repository/JobUploadsRepository.ts +35 -0
- package/src/domains/job/S3Repository/S3RepositoryBase.spec.ts +13 -19
- package/src/domains/job/S3Repository/S3RepositoryBase.ts +24 -9
- package/src/domains/job/S3Repository/index.ts +2 -1
- package/src/domains/job/constants/InjectTokens.ts +8 -0
- package/src/domains/job/constants/JobExecutionStatus.constants.ts +1 -0
- package/src/domains/job/constants/Repository.constants.ts +3 -3
- package/src/domains/job/constants/RetryConfig.constants.ts +29 -0
- package/src/domains/job/core/pagination/services/PaginationService.spec.ts +3 -5
- package/src/domains/job/index.ts +6 -4
- package/src/domains/job/manifest/JobManifestTypes.ts +8 -6
- package/src/domains/job/manifest/mocks/JobManifest.mock.ts +2 -2
- package/src/domains/job/manifest/mocks/JobManifestDto.mock.ts +3 -2
- package/src/domains/job/manifest/mocks/JobManifestRecord.mock.ts +4 -3
- package/src/domains/job/model/Base/Base.ts +11 -5
- package/src/domains/job/model/Job/Job.ts +11 -6
- package/src/domains/job/model/Job/mocks/Job.mock.ts +2 -2
- package/src/domains/job/model/Job/mocks/JobDto.mock.ts +3 -2
- package/src/domains/job/model/Job/mocks/JobRecord.mock.ts +3 -2
- package/src/domains/job/model/JobContext/JobContext.ts +7 -5
- package/src/domains/job/model/JobContext/mocks/JobContext.mock.ts +2 -2
- package/src/domains/job/model/JobContext/mocks/JobContextDto.mock.ts +3 -2
- package/src/domains/job/model/JobContext/mocks/JobContextRecord.mock.ts +3 -2
- package/src/domains/job/model/JobExecution/JobExecution.ts +74 -9
- package/src/domains/job/model/JobExecution/mocks/JobExecution.mock.ts +7 -4
- package/src/domains/job/model/JobExecution/mocks/JobExecutionDto.mock.ts +32 -16
- package/src/domains/job/model/JobExecution/mocks/JobExecutionRecord.mock.ts +10 -4
- package/src/domains/job/model/LogPayload/LogPayload.ts +32 -11
- package/src/domains/job/model/LogPayload/index.ts +1 -0
- package/src/domains/job/model/LogPayload/mocks/LogPayload.mock.ts +21 -0
- package/src/domains/job/repository/AbstractRepository.ts +91 -59
- package/src/domains/job/repository/JobContextRepository/JobContextRepository.spec.ts +39 -49
- package/src/domains/job/repository/JobContextRepository/JobContextRepository.ts +33 -21
- package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.spec.ts +343 -169
- package/src/domains/job/repository/JobExecutionRepository/JobExecutionRepository.ts +112 -141
- package/src/domains/job/repository/JobExecutionRepository/utils/convertDynamoDbItemToJobExecution/convertDynamoDbItemToJobExecution.ts +22 -0
- package/src/domains/job/repository/JobExecutionRepository/utils/index.ts +1 -0
- package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.spec.ts +23 -38
- package/src/domains/job/repository/JobManifestRepository/JobManifestRepository.ts +29 -36
- package/src/domains/job/repository/JobRepository/JobRepository.spec.ts +72 -80
- package/src/domains/job/repository/JobRepository/JobRepository.ts +66 -70
- package/src/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.spec.ts +60 -0
- package/src/domains/job/repository/MultiTenantJobExecutionRepository/MultiTenantJobExecutionRepository.ts +42 -0
- package/src/domains/job/repository/index.ts +1 -0
- package/src/domains/job/services/JobContextService/JobContextService.spec.ts +73 -39
- package/src/domains/job/services/JobContextService/JobContextService.ts +37 -6
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.defs.ts +1 -3
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.spec.ts +113 -31
- package/src/domains/job/services/JobExecutionTaskService/JobExecutionTaskService.ts +101 -49
- package/src/domains/job/services/JobManifestService/JobManifestService.spec.ts +4 -41
- package/src/domains/job/services/JobManifestService/JobManifestService.ts +2 -8
- package/src/domains/job/services/JobStartQueueService/JobStartQueueService.defs.ts +27 -0
- package/src/domains/job/services/JobStartQueueService/JobStartQueueService.ts +42 -0
- package/src/domains/job/services/JobUploadService/JobUploadService.spec.ts +1 -1
- package/src/domains/job/services/JobUploadService/JobUploadService.ts +1 -1
- package/src/domains/job/services/JobUploadService/utils/manifestValidator.spec.ts +12 -25
- package/src/domains/job/services/JobUploadService/utils/manifestValidator.ts +2 -7
- package/src/domains/job/services/index.ts +2 -1
- package/src/domains/job/utils/SlackClient/SlackClient.spec.ts +0 -2
- package/src/domains/manifest/services/ManifestServiceForDev.spec.ts +1 -2
- package/src/domains/manifest/services/ManifestServiceForDev.ts +0 -4
- package/src/index.ts +1 -0
- package/src/jest-global-setup.ts +8 -4
- package/src/mocks/mockIoc.ts +18 -43
- package/src/utils/buildJobRunnerModule/buildJobRunnerModule.ts +81 -0
- package/src/utils/buildJobRunnerModule/buildJobRunnerModule.types.ts +13 -0
- package/src/utils/index.ts +2 -0
- package/lib/domains/job/S3Repository/S3Repository.d.ts +0 -15
- package/lib/domains/job/S3Repository/S3Repository.spec.d.ts +0 -5
- package/lib/domains/job/services/JobQueueService/JobQueueService.d.ts +0 -9
- package/lib/domains/job/services/JobQueueService/JobQueueService.defs.d.ts +0 -9
- package/src/custom-types.d.ts +0 -7
- package/src/domains/job/S3Repository/S3Repository.spec.ts +0 -50
- package/src/domains/job/S3Repository/S3Repository.ts +0 -27
- package/src/domains/job/services/JobQueueService/JobQueueService.defs.ts +0 -10
- package/src/domains/job/services/JobQueueService/JobQueueService.spec.ts +0 -190
- package/src/domains/job/services/JobQueueService/JobQueueService.ts +0 -60
|
@@ -3,39 +3,56 @@
|
|
|
3
3
|
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { ConflictException, ECSClient, LaunchType, RunTaskCommand } from '@aws-sdk/client-ecs';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { JobDetailsRepository, JobUploadsRepository } from '../../S3Repository';
|
|
9
|
+
import { JOB_EXECUTION_STATUS } from '../../constants/JobExecutionStatus.constants';
|
|
10
10
|
import { JobExecution } from '../../model/JobExecution';
|
|
11
|
-
import {
|
|
11
|
+
import { JobExecutionRepository } from '../../repository';
|
|
12
|
+
import { getAwsTags } from '../../utils';
|
|
12
13
|
import { JobContextService } from '../JobContextService/JobContextService';
|
|
14
|
+
import { JobManifestService } from '../JobManifestService/JobManifestService';
|
|
15
|
+
import { HasReachedMaxTenantConcurrencyParams, RunJobTaskParams } from './JobExecutionTaskService.defs';
|
|
13
16
|
|
|
14
17
|
export class JobExecutionTaskService {
|
|
15
18
|
constructor(
|
|
16
19
|
private readonly ecsClient: ECSClient,
|
|
17
20
|
private readonly jobManifestService: JobManifestService,
|
|
18
21
|
private readonly jobContextService: JobContextService,
|
|
22
|
+
private readonly jobExecutionRepository: JobExecutionRepository,
|
|
19
23
|
private readonly jobDetailsRepository: JobDetailsRepository,
|
|
20
|
-
private readonly jobUploadRepository:
|
|
24
|
+
private readonly jobUploadRepository: JobUploadsRepository,
|
|
21
25
|
) {}
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return
|
|
27
|
+
public async hasReachedMaxTenantConcurrency(params: HasReachedMaxTenantConcurrencyParams): Promise<boolean> {
|
|
28
|
+
const currentRunningExecutions = await this.jobExecutionRepository.list({
|
|
29
|
+
consistentRead: true,
|
|
30
|
+
limit: params.maxTenantConcurrentTasks,
|
|
31
|
+
filters: {
|
|
32
|
+
status: JOB_EXECUTION_STATUS.running,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return currentRunningExecutions.length >= params.maxTenantConcurrentTasks;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
public async
|
|
36
|
-
const
|
|
39
|
+
public async hasReachedMaxJobConcurrency(jobExecution: JobExecution): Promise<boolean> {
|
|
40
|
+
const jobManifest = await this.jobManifestService.findJobManifest(jobExecution.jobName, jobExecution.version);
|
|
41
|
+
|
|
42
|
+
if (!jobManifest || typeof jobManifest.concurrency !== 'number') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const runningExecutions = await this.jobExecutionRepository.list({
|
|
47
|
+
consistentRead: true,
|
|
48
|
+
limit: jobManifest.concurrency,
|
|
49
|
+
filters: {
|
|
50
|
+
jobName: jobExecution.jobName,
|
|
51
|
+
status: JOB_EXECUTION_STATUS.running,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
37
54
|
|
|
38
|
-
return
|
|
55
|
+
return runningExecutions.length >= jobManifest.concurrency;
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
public async runJobExecutionTask(params: RunJobTaskParams): Promise<string | undefined> {
|
|
@@ -74,7 +91,7 @@ export class JobExecutionTaskService {
|
|
|
74
91
|
{
|
|
75
92
|
name: 'JOB_CODE_ARTIFACT_URL',
|
|
76
93
|
value: await this.jobUploadRepository.getS3SignedUrl(
|
|
77
|
-
`${params.jobExecution.jobName}/${params.jobExecution.version}/export.zip`,
|
|
94
|
+
`${params.tenantId}/${params.jobExecution.jobName}/${params.jobExecution.version}/export.zip`,
|
|
78
95
|
),
|
|
79
96
|
},
|
|
80
97
|
{
|
|
@@ -83,49 +100,84 @@ export class JobExecutionTaskService {
|
|
|
83
100
|
},
|
|
84
101
|
];
|
|
85
102
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
},
|
|
94
|
-
launchType: LaunchType.FARGATE,
|
|
95
|
-
taskDefinition: params.taskDefinitionArn,
|
|
96
|
-
overrides: {
|
|
97
|
-
containerOverrides: [
|
|
98
|
-
{
|
|
99
|
-
name: 'job-runtime-container',
|
|
100
|
-
environment: environmentVariables,
|
|
103
|
+
try {
|
|
104
|
+
const result = await this.ecsClient.send(
|
|
105
|
+
new RunTaskCommand({
|
|
106
|
+
cluster: params.clusterArn,
|
|
107
|
+
networkConfiguration: {
|
|
108
|
+
awsvpcConfiguration: {
|
|
109
|
+
subnets: params.subnets,
|
|
101
110
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
},
|
|
112
|
+
launchType: LaunchType.FARGATE,
|
|
113
|
+
taskDefinition: params.taskDefinitionArn,
|
|
114
|
+
overrides: {
|
|
115
|
+
containerOverrides: [
|
|
116
|
+
{
|
|
117
|
+
name: 'job-runtime-container',
|
|
118
|
+
environment: environmentVariables,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
tags: Object.entries(tags).map(([key, value]) => ({
|
|
106
123
|
key,
|
|
107
124
|
value,
|
|
108
125
|
})),
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
clientToken: this.computeJobExecutionClientToken(params),
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
112
129
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
// This task ARN can be used to terminate the job task.
|
|
131
|
+
// If no error is returned, assume task has started.
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
133
|
+
const task = result.tasks![0];
|
|
134
|
+
return task.taskArn;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Task already started with the provided "clientToken", return the previously started task ARN.
|
|
137
|
+
if (error instanceof ConflictException) {
|
|
138
|
+
if (error.resourceIds?.[0]) {
|
|
139
|
+
console.warn(
|
|
140
|
+
`Execution already started, returning previous task ARN: ${error.resourceIds[0]} ` +
|
|
141
|
+
`(id: "${params.jobExecution.id}", job: ${params.jobExecution.jobName}, tenant: ${params.tenantId})`,
|
|
142
|
+
);
|
|
143
|
+
return error.resourceIds[0];
|
|
144
|
+
} else {
|
|
145
|
+
// In some instances the previous task ARN isn't returned.
|
|
146
|
+
console.warn(
|
|
147
|
+
`Execution already started, no previous task ARN returned ` +
|
|
148
|
+
`(id: "${params.jobExecution.id}", job: ${params.jobExecution.jobName}, tenant: ${params.tenantId})`,
|
|
149
|
+
);
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private computeJobExecutionClientToken(params: RunJobTaskParams): string {
|
|
159
|
+
// https://docs.aws.amazon.com/AmazonECS/latest/APIReference/ECS_Idempotency.html#RunTaskIdempotency
|
|
160
|
+
// Include retryCount so that retry attempts get a unique clientToken and create new tasks
|
|
161
|
+
// Without this, ECS would return the old failed task ARN instead of creating a new task
|
|
162
|
+
const retryCount = params.jobExecution.retryCount ?? 0;
|
|
163
|
+
const key = `jobExecution~${params.tenantId}~${params.jobExecution.jobName}~${params.jobExecution.id}~${retryCount}`;
|
|
164
|
+
|
|
165
|
+
return createHash('sha1').update(key).digest('hex');
|
|
118
166
|
}
|
|
119
167
|
|
|
120
168
|
private async createJobDetailsFile(tenantId: string, executionToStart: JobExecution): Promise<string> {
|
|
121
|
-
const jobDetailsFileName = JobDetailsRepository.createJobDetailsFileName(tenantId, executionToStart.id);
|
|
122
169
|
const jobManifest = await this.jobManifestService.findJobManifest(
|
|
123
170
|
executionToStart.jobName,
|
|
124
171
|
executionToStart.version,
|
|
125
172
|
);
|
|
126
173
|
const jobContext = await this.jobContextService.findJobContext(executionToStart.context);
|
|
127
174
|
|
|
128
|
-
await this.jobDetailsRepository.createJobDetailsFile(
|
|
175
|
+
const jobDetailsFileName = await this.jobDetailsRepository.createJobDetailsFile(
|
|
176
|
+
tenantId,
|
|
177
|
+
jobContext,
|
|
178
|
+
jobManifest,
|
|
179
|
+
executionToStart,
|
|
180
|
+
);
|
|
129
181
|
return await this.jobDetailsRepository.getS3SignedUrl(jobDetailsFileName);
|
|
130
182
|
}
|
|
131
183
|
}
|
|
@@ -4,14 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { initIocContainer, iocContainer } from '../../../../mocks/mockIoc';
|
|
6
6
|
import {
|
|
7
|
-
DeleteItemCommandOutput,
|
|
8
7
|
DynamoDBClient,
|
|
9
8
|
DynamoDBClientConfig,
|
|
10
9
|
GetItemCommandOutput,
|
|
11
10
|
PutItemCommandOutput,
|
|
12
11
|
ScanCommandOutput,
|
|
13
12
|
} from '@aws-sdk/client-dynamodb';
|
|
14
|
-
import { testLogger } from '../../../../jest-global-setup';
|
|
15
13
|
import { JobManifestService } from './JobManifestService';
|
|
16
14
|
import {
|
|
17
15
|
createMockCreateJobManifestRequest,
|
|
@@ -26,10 +24,9 @@ import {
|
|
|
26
24
|
createMockClientPutResponse,
|
|
27
25
|
createMockClientScanResponse,
|
|
28
26
|
} from '../../core/dynamoClient.mock';
|
|
29
|
-
import {
|
|
27
|
+
import { ENTITY_TYPES } from '../../constants/Repository.constants';
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
initIocContainer(testLogger);
|
|
29
|
+
initIocContainer();
|
|
33
30
|
|
|
34
31
|
// Mock DynamoDB Client
|
|
35
32
|
const mockOptions = {} as DynamoDBClientConfig;
|
|
@@ -49,15 +46,11 @@ describe('JobManifestService', (): void => {
|
|
|
49
46
|
});
|
|
50
47
|
|
|
51
48
|
describe(`createJobManifest`, (): void => {
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
jest.resetAllMocks();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
49
|
it(`should return a response for createJob`, async (): Promise<void> => {
|
|
57
50
|
const newJobManifestToCreate = createMockCreateJobManifestRequest();
|
|
58
51
|
const mockReponse = createMockClientPutResponse({
|
|
59
52
|
Attributes: createMockJobManifestDto({
|
|
60
|
-
pk:
|
|
53
|
+
pk: ENTITY_TYPES.manifest,
|
|
61
54
|
sk: newJobManifestToCreate.name,
|
|
62
55
|
}),
|
|
63
56
|
});
|
|
@@ -68,34 +61,12 @@ describe('JobManifestService', (): void => {
|
|
|
68
61
|
expect(mockSend).toBeCalledTimes(1);
|
|
69
62
|
expect(response).toStrictEqual({
|
|
70
63
|
...newJobManifestToCreate,
|
|
71
|
-
type:
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe(`deleteJobManifest`, (): void => {
|
|
77
|
-
beforeEach(() => {
|
|
78
|
-
jest.resetAllMocks();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it(`should return an empty response for job manifest deletion`, async (): Promise<void> => {
|
|
82
|
-
mockSend.mockImplementationOnce((): DeleteItemCommandOutput => {
|
|
83
|
-
return {
|
|
84
|
-
$metadata: {},
|
|
85
|
-
};
|
|
64
|
+
type: ENTITY_TYPES.manifest,
|
|
86
65
|
});
|
|
87
|
-
|
|
88
|
-
const response = await jobManifestService.deleteJobManifest(`name`);
|
|
89
|
-
expect(response).toBe(undefined);
|
|
90
|
-
expect(mockSend).toBeCalledTimes(1);
|
|
91
66
|
});
|
|
92
67
|
});
|
|
93
68
|
|
|
94
69
|
describe(`findJobManifest`, (): void => {
|
|
95
|
-
beforeEach(() => {
|
|
96
|
-
jest.resetAllMocks();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
70
|
it(`should return job manifest`, async (): Promise<void> => {
|
|
100
71
|
const mockJobManifestItem = createMockJobManifestDto();
|
|
101
72
|
mockSend.mockImplementationOnce(
|
|
@@ -136,10 +107,6 @@ describe('JobManifestService', (): void => {
|
|
|
136
107
|
});
|
|
137
108
|
|
|
138
109
|
describe(`listAllJobManifests`, (): void => {
|
|
139
|
-
beforeEach(() => {
|
|
140
|
-
jest.resetAllMocks();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
110
|
it(`should return an empty list`, async (): Promise<void> => {
|
|
144
111
|
mockSend.mockImplementationOnce((): ScanCommandOutput => createMockClientScanResponse());
|
|
145
112
|
|
|
@@ -191,10 +158,6 @@ describe('JobManifestService', (): void => {
|
|
|
191
158
|
});
|
|
192
159
|
|
|
193
160
|
describe(`updateJobManifest`, (): void => {
|
|
194
|
-
beforeEach(() => {
|
|
195
|
-
jest.resetAllMocks();
|
|
196
|
-
});
|
|
197
|
-
|
|
198
161
|
it(`should update a job manifest record`, async (): Promise<void> => {
|
|
199
162
|
const jobManifestToUpdate = createMockJobManifest({
|
|
200
163
|
name: `name`,
|
|
@@ -21,8 +21,7 @@ export class JobManifestService {
|
|
|
21
21
|
version: string,
|
|
22
22
|
input: UpdateJobManifestRequest,
|
|
23
23
|
): Promise<JobManifest | null> {
|
|
24
|
-
|
|
25
|
-
return this.jobManifestRepository.update(sk, input);
|
|
24
|
+
return this.jobManifestRepository.update({ name, version }, input);
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
public async listAllJobManifests(): Promise<Array<JobManifest>> {
|
|
@@ -30,11 +29,6 @@ export class JobManifestService {
|
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
public async findJobManifest(name: string, version: string): Promise<JobManifest | null> {
|
|
33
|
-
|
|
34
|
-
return this.jobManifestRepository.findOne(sk);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
public async deleteJobManifest(name: string): Promise<void> {
|
|
38
|
-
return await this.jobManifestRepository.delete(name);
|
|
32
|
+
return this.jobManifestRepository.findOne({ name, version });
|
|
39
33
|
}
|
|
40
34
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Message body for the job start queue.
|
|
8
|
+
*/
|
|
9
|
+
export interface JobStartMessage {
|
|
10
|
+
/** The tenant ID */
|
|
11
|
+
tenantId: string;
|
|
12
|
+
/** The job execution ID to start */
|
|
13
|
+
executionId: string;
|
|
14
|
+
/** The job name */
|
|
15
|
+
jobName: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parameters for sending a job start message.
|
|
20
|
+
*/
|
|
21
|
+
export interface SendJobStartMessageParams {
|
|
22
|
+
tenantId: string;
|
|
23
|
+
executionId: string;
|
|
24
|
+
jobName: string;
|
|
25
|
+
/** Delay in seconds before the message becomes visible (0-900, max 15 minutes) */
|
|
26
|
+
delaySeconds?: number;
|
|
27
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
|
|
7
|
+
import { inject, injectable } from 'inversify';
|
|
8
|
+
import { InjectTokens } from '../../constants/InjectTokens';
|
|
9
|
+
import { JobStartMessage, SendJobStartMessageParams } from './JobStartQueueService.defs';
|
|
10
|
+
|
|
11
|
+
@injectable()
|
|
12
|
+
export class JobStartQueueService {
|
|
13
|
+
public constructor(
|
|
14
|
+
@inject(SQSClient) private readonly sqsClient: SQSClient,
|
|
15
|
+
@inject(InjectTokens.JobStartQueueUrl) private readonly queueUrl: string,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sends a job start message to the SQS queue.
|
|
20
|
+
* Uses a standard queue (not FIFO) to support DelaySeconds for retry backoff.
|
|
21
|
+
* Duplicate job starts are prevented by ECS RunTask clientToken.
|
|
22
|
+
*/
|
|
23
|
+
public async sendJobStartMessage(params: SendJobStartMessageParams): Promise<void> {
|
|
24
|
+
const message: JobStartMessage = {
|
|
25
|
+
tenantId: params.tenantId,
|
|
26
|
+
executionId: params.executionId,
|
|
27
|
+
jobName: params.jobName,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
await this.sqsClient.send(
|
|
31
|
+
new SendMessageCommand({
|
|
32
|
+
QueueUrl: this.queueUrl,
|
|
33
|
+
MessageBody: JSON.stringify(message),
|
|
34
|
+
DelaySeconds: params.delaySeconds ?? 0,
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
console.log(
|
|
39
|
+
`Sent job start message for execution ${params.executionId} (job: ${params.jobName}, tenant: ${params.tenantId}, delay: ${params.delaySeconds ?? 0}s)`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -91,7 +91,7 @@ describe('JobUploadService', (): void => {
|
|
|
91
91
|
await jobUploadService.uploadJobFolder(mockAxiosClient, mockFolderPath, mockBaseTempDir);
|
|
92
92
|
|
|
93
93
|
expect(mockMkdtemp).toHaveBeenCalledWith('/tmp/job-upload');
|
|
94
|
-
expect(mockGetAndValidateManifest).toHaveBeenCalledWith(mockAxiosClient, mockFolderPath
|
|
94
|
+
expect(mockGetAndValidateManifest).toHaveBeenCalledWith(mockAxiosClient, mockFolderPath);
|
|
95
95
|
expect(mockInitializeUpload).toHaveBeenCalledWith(
|
|
96
96
|
mockAxiosClient,
|
|
97
97
|
mockFolderPath,
|
|
@@ -37,7 +37,7 @@ export class JobUploadService {
|
|
|
37
37
|
const tmpDir = await fsp.mkdtemp(path.resolve(baseTempDir, 'job-upload'));
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
|
-
const manifest = await getAndValidateManifest(apiClient, folderPath
|
|
40
|
+
const manifest = await getAndValidateManifest(apiClient, folderPath);
|
|
41
41
|
|
|
42
42
|
const initialUpload = await initializeUpload(apiClient, folderPath, tmpDir, this.logger);
|
|
43
43
|
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Logger } from '@squiz/dx-logger-lib';
|
|
7
6
|
import { AxiosInstance } from 'axios';
|
|
8
7
|
import path from 'path';
|
|
9
8
|
import { JobManifestV1, ManifestServiceForDev } from '../../../../manifest';
|
|
@@ -24,7 +23,6 @@ jest.mock('path', () => ({
|
|
|
24
23
|
}));
|
|
25
24
|
|
|
26
25
|
describe('manifestValidator', () => {
|
|
27
|
-
let mockLogger: Logger;
|
|
28
26
|
let mockApiClient: AxiosInstance;
|
|
29
27
|
let mockManifestService: jest.Mocked<ManifestServiceForDev>;
|
|
30
28
|
let mockManifest: jest.Mocked<JobManifestV1>;
|
|
@@ -34,13 +32,6 @@ describe('manifestValidator', () => {
|
|
|
34
32
|
beforeEach(() => {
|
|
35
33
|
jest.clearAllMocks();
|
|
36
34
|
|
|
37
|
-
mockLogger = {
|
|
38
|
-
error: jest.fn(),
|
|
39
|
-
info: jest.fn(),
|
|
40
|
-
warn: jest.fn(),
|
|
41
|
-
debug: jest.fn(),
|
|
42
|
-
} as any;
|
|
43
|
-
|
|
44
35
|
mockApiClient = {
|
|
45
36
|
get: jest.fn(),
|
|
46
37
|
post: jest.fn(),
|
|
@@ -75,9 +66,9 @@ describe('manifestValidator', () => {
|
|
|
75
66
|
it('should successfully validate manifest when version does not exist', async () => {
|
|
76
67
|
mockCheckIfVersionExists.mockResolvedValue(false);
|
|
77
68
|
|
|
78
|
-
const result = await getAndValidateManifest(mockApiClient, testFolderPath
|
|
69
|
+
const result = await getAndValidateManifest(mockApiClient, testFolderPath);
|
|
79
70
|
|
|
80
|
-
expect(ManifestServiceForDev).toHaveBeenCalledWith(
|
|
71
|
+
expect(ManifestServiceForDev).toHaveBeenCalledWith();
|
|
81
72
|
expect(mockPathJoin).toHaveBeenCalledWith(testFolderPath, 'manifest.json');
|
|
82
73
|
expect(mockManifestService.readJobManifest).toHaveBeenCalledWith(testManifestPath);
|
|
83
74
|
expect(mockCheckIfVersionExists).toHaveBeenCalledWith(mockApiClient, `/job/${testJobName}/${testJobVersion}`);
|
|
@@ -87,7 +78,7 @@ describe('manifestValidator', () => {
|
|
|
87
78
|
it('should throw error when version already exists', async () => {
|
|
88
79
|
mockCheckIfVersionExists.mockResolvedValue(true);
|
|
89
80
|
|
|
90
|
-
await expect(getAndValidateManifest(mockApiClient, testFolderPath
|
|
81
|
+
await expect(getAndValidateManifest(mockApiClient, testFolderPath)).rejects.toThrow(
|
|
91
82
|
`Cannot upload job version, ${testJobName} ${testJobVersion} already exists`,
|
|
92
83
|
);
|
|
93
84
|
|
|
@@ -99,9 +90,7 @@ describe('manifestValidator', () => {
|
|
|
99
90
|
const manifestError = new Error('Failed to read manifest');
|
|
100
91
|
mockManifestService.readJobManifest.mockRejectedValue(manifestError);
|
|
101
92
|
|
|
102
|
-
await expect(getAndValidateManifest(mockApiClient, testFolderPath
|
|
103
|
-
'Failed to read manifest',
|
|
104
|
-
);
|
|
93
|
+
await expect(getAndValidateManifest(mockApiClient, testFolderPath)).rejects.toThrow('Failed to read manifest');
|
|
105
94
|
|
|
106
95
|
expect(mockManifestService.readJobManifest).toHaveBeenCalledWith(testManifestPath);
|
|
107
96
|
expect(mockCheckIfVersionExists).not.toHaveBeenCalled();
|
|
@@ -111,9 +100,7 @@ describe('manifestValidator', () => {
|
|
|
111
100
|
const versionCheckError = new Error('Version check failed');
|
|
112
101
|
mockCheckIfVersionExists.mockRejectedValue(versionCheckError);
|
|
113
102
|
|
|
114
|
-
await expect(getAndValidateManifest(mockApiClient, testFolderPath
|
|
115
|
-
'Version check failed',
|
|
116
|
-
);
|
|
103
|
+
await expect(getAndValidateManifest(mockApiClient, testFolderPath)).rejects.toThrow('Version check failed');
|
|
117
104
|
|
|
118
105
|
expect(mockManifestService.readJobManifest).toHaveBeenCalledWith(testManifestPath);
|
|
119
106
|
expect(mockCheckIfVersionExists).toHaveBeenCalledWith(mockApiClient, `/job/${testJobName}/${testJobVersion}`);
|
|
@@ -127,7 +114,7 @@ describe('manifestValidator', () => {
|
|
|
127
114
|
mockManifest.getVersion.mockReturnValue(specialJobVersion);
|
|
128
115
|
mockCheckIfVersionExists.mockResolvedValue(false);
|
|
129
116
|
|
|
130
|
-
const result = await getAndValidateManifest(mockApiClient, testFolderPath
|
|
117
|
+
const result = await getAndValidateManifest(mockApiClient, testFolderPath);
|
|
131
118
|
|
|
132
119
|
expect(mockCheckIfVersionExists).toHaveBeenCalledWith(
|
|
133
120
|
mockApiClient,
|
|
@@ -141,7 +128,7 @@ describe('manifestValidator', () => {
|
|
|
141
128
|
mockManifest.getVersion.mockReturnValue(testJobVersion);
|
|
142
129
|
mockCheckIfVersionExists.mockResolvedValue(false);
|
|
143
130
|
|
|
144
|
-
const result = await getAndValidateManifest(mockApiClient, testFolderPath
|
|
131
|
+
const result = await getAndValidateManifest(mockApiClient, testFolderPath);
|
|
145
132
|
|
|
146
133
|
expect(mockCheckIfVersionExists).toHaveBeenCalledWith(mockApiClient, `/job//${testJobVersion}`);
|
|
147
134
|
expect(result).toBe(mockManifest);
|
|
@@ -152,7 +139,7 @@ describe('manifestValidator', () => {
|
|
|
152
139
|
mockManifest.getVersion.mockReturnValue('');
|
|
153
140
|
mockCheckIfVersionExists.mockResolvedValue(false);
|
|
154
141
|
|
|
155
|
-
const result = await getAndValidateManifest(mockApiClient, testFolderPath
|
|
142
|
+
const result = await getAndValidateManifest(mockApiClient, testFolderPath);
|
|
156
143
|
|
|
157
144
|
expect(mockCheckIfVersionExists).toHaveBeenCalledWith(mockApiClient, `/job/${testJobName}/`);
|
|
158
145
|
expect(result).toBe(mockManifest);
|
|
@@ -165,7 +152,7 @@ describe('manifestValidator', () => {
|
|
|
165
152
|
mockPathJoin.mockReturnValue(expectedManifestPath);
|
|
166
153
|
mockCheckIfVersionExists.mockResolvedValue(false);
|
|
167
154
|
|
|
168
|
-
await getAndValidateManifest(mockApiClient, relativePath
|
|
155
|
+
await getAndValidateManifest(mockApiClient, relativePath);
|
|
169
156
|
|
|
170
157
|
expect(mockPathJoin).toHaveBeenCalledWith(relativePath, 'manifest.json');
|
|
171
158
|
expect(mockManifestService.readJobManifest).toHaveBeenCalledWith(expectedManifestPath);
|
|
@@ -178,7 +165,7 @@ describe('manifestValidator', () => {
|
|
|
178
165
|
mockPathJoin.mockReturnValue(expectedManifestPath);
|
|
179
166
|
mockCheckIfVersionExists.mockResolvedValue(false);
|
|
180
167
|
|
|
181
|
-
await getAndValidateManifest(mockApiClient, folderPathWithSlash
|
|
168
|
+
await getAndValidateManifest(mockApiClient, folderPathWithSlash);
|
|
182
169
|
|
|
183
170
|
expect(mockPathJoin).toHaveBeenCalledWith(folderPathWithSlash, 'manifest.json');
|
|
184
171
|
expect(mockManifestService.readJobManifest).toHaveBeenCalledWith(expectedManifestPath);
|
|
@@ -192,7 +179,7 @@ describe('manifestValidator', () => {
|
|
|
192
179
|
mockManifest.getVersion.mockReturnValue(complexJobVersion);
|
|
193
180
|
mockCheckIfVersionExists.mockResolvedValue(false);
|
|
194
181
|
|
|
195
|
-
await getAndValidateManifest(mockApiClient, testFolderPath
|
|
182
|
+
await getAndValidateManifest(mockApiClient, testFolderPath);
|
|
196
183
|
|
|
197
184
|
expect(mockCheckIfVersionExists).toHaveBeenCalledWith(
|
|
198
185
|
mockApiClient,
|
|
@@ -205,7 +192,7 @@ describe('manifestValidator', () => {
|
|
|
205
192
|
mockManifest.getVersion.mockReturnValue(undefined as any);
|
|
206
193
|
mockCheckIfVersionExists.mockResolvedValue(false);
|
|
207
194
|
|
|
208
|
-
await getAndValidateManifest(mockApiClient, testFolderPath
|
|
195
|
+
await getAndValidateManifest(mockApiClient, testFolderPath);
|
|
209
196
|
|
|
210
197
|
expect(mockCheckIfVersionExists).toHaveBeenCalledWith(mockApiClient, `/job/${null}/${undefined}`);
|
|
211
198
|
});
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Logger } from '@squiz/dx-logger-lib';
|
|
7
6
|
import { AxiosInstance } from 'axios';
|
|
8
7
|
import path from 'path';
|
|
9
8
|
import { JobManifestV1, ManifestServiceForDev } from '../../../../manifest';
|
|
@@ -16,12 +15,8 @@ import { checkIfVersionExists } from './versionChecker';
|
|
|
16
15
|
* @param logger - Logger instance
|
|
17
16
|
* @returns The validated manifest
|
|
18
17
|
*/
|
|
19
|
-
export async function getAndValidateManifest(
|
|
20
|
-
|
|
21
|
-
folderPath: string,
|
|
22
|
-
logger: Logger,
|
|
23
|
-
): Promise<JobManifestV1> {
|
|
24
|
-
const manifestService = new ManifestServiceForDev(logger);
|
|
18
|
+
export async function getAndValidateManifest(apiClient: AxiosInstance, folderPath: string): Promise<JobManifestV1> {
|
|
19
|
+
const manifestService = new ManifestServiceForDev();
|
|
25
20
|
const manifestPath = path.join(folderPath, 'manifest.json');
|
|
26
21
|
|
|
27
22
|
const manifest = await manifestService.readJobManifest(manifestPath);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './JobContextService/JobContextService';
|
|
2
2
|
export * from './JobExecutionTaskService/JobExecutionTaskService';
|
|
3
3
|
export * from './JobManifestService/JobManifestService';
|
|
4
|
-
export * from './
|
|
4
|
+
export * from './JobStartQueueService/JobStartQueueService';
|
|
5
|
+
export * from './JobStartQueueService/JobStartQueueService.defs';
|
|
5
6
|
export * from './JobUploadService/JobUploadService';
|
|
@@ -3,10 +3,9 @@ import { ManifestServiceForDev } from './ManifestServiceForDev';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { ManifestValidationError } from '../errors/ManifestValidationError';
|
|
5
5
|
import { ResourceNotFoundError } from '@squiz/dx-common-lib';
|
|
6
|
-
import { getLogger } from '@squiz/dx-logger-lib';
|
|
7
6
|
|
|
8
7
|
describe('ManifestServiceForDev', () => {
|
|
9
|
-
const service = new ManifestServiceForDev(
|
|
8
|
+
const service = new ManifestServiceForDev();
|
|
10
9
|
|
|
11
10
|
describe('readJobManifest', () => {
|
|
12
11
|
it('should read and validate a job manifest from a file path', async () => {
|
|
@@ -4,15 +4,11 @@ import { JobManifestModel } from '../ManifestModel';
|
|
|
4
4
|
import { getAndAssertJobManifestVersion } from '../utils/getAndAssertJobManifestVersion';
|
|
5
5
|
import { JobManifestV1 } from '../../job/manifest/JobManifestV1';
|
|
6
6
|
import { JsonValidationService } from '@squiz/dx-json-schema-lib';
|
|
7
|
-
|
|
8
7
|
import fsExtra from 'fs-extra';
|
|
9
|
-
import { Logger } from '@squiz/dx-logger-lib';
|
|
10
8
|
|
|
11
9
|
export class ManifestServiceForDev {
|
|
12
10
|
protected jobValidationService: JsonValidationService = new JsonValidationService();
|
|
13
11
|
|
|
14
|
-
constructor(protected logger: Logger) {}
|
|
15
|
-
|
|
16
12
|
protected getJobManifest(manifestModel: JobManifestModel, manifestPath: string): JobManifestV1 {
|
|
17
13
|
const schemaVersion = getAndAssertJobManifestVersion(manifestModel, JobManifestV1.getJobSchemaVersionRegex());
|
|
18
14
|
if (schemaVersion === 'JobV1') {
|
package/src/index.ts
CHANGED
package/src/jest-global-setup.ts
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* @license
|
|
3
3
|
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { Logger } from '@squiz/optimization-logger';
|
|
6
6
|
|
|
7
|
-
export const testLogger =
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export const testLogger: Logger = {
|
|
8
|
+
audit: jest.fn(),
|
|
9
|
+
error: jest.fn(),
|
|
10
|
+
warn: jest.fn(),
|
|
11
|
+
info: jest.fn(),
|
|
12
|
+
debug: jest.fn(),
|
|
13
|
+
};
|