@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.
Files changed (137) hide show
  1. package/CHANGELOG.md +12 -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
@@ -3,39 +3,56 @@
3
3
  * Copyright Squiz Australia Pty Ltd. All Rights Reserved.
4
4
  */
5
5
 
6
- import { DesiredStatus, ECSClient, LaunchType, ListTasksCommand, RunTaskCommand } from '@aws-sdk/client-ecs';
7
- import { getAwsTags } from '../../utils';
8
- import { ListRunningTasksParams, RunJobTaskParams } from './JobExecutionTaskService.defs';
9
- import { JobDetailsRepository, S3Repository } from '../../S3Repository';
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 { JobManifestService } from '../JobManifestService/JobManifestService';
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: S3Repository,
24
+ private readonly jobUploadRepository: JobUploadsRepository,
21
25
  ) {}
22
26
 
23
- protected async getTenantRunningTasks(params: ListRunningTasksParams): Promise<number> {
24
- const input = {
25
- cluster: params.clusterArn,
26
- family: params.runtimeTaskFamily,
27
- maxResults: params.maxTenantConcurrentTasks + 1,
28
- desiredStatus: DesiredStatus.RUNNING,
29
- };
30
- const command = new ListTasksCommand(input);
31
- const response = await this.ecsClient.send(command);
32
- return response.taskArns?.length ?? 0;
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 hasReachedMaxTenantConcurrency(params: ListRunningTasksParams): Promise<boolean> {
36
- const currentRunningTasks = await this.getTenantRunningTasks(params);
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 currentRunningTasks > params.maxTenantConcurrentTasks;
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
- const result = await this.ecsClient.send(
87
- new RunTaskCommand({
88
- cluster: params.clusterArn,
89
- networkConfiguration: {
90
- awsvpcConfiguration: {
91
- subnets: params.subnets,
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
- tags: [
105
- ...Object.entries(tags).map(([key, value]) => ({
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
- // This task ARN can be used to terminate the job task.
114
- // If no error is returned, assume task has started.
115
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
116
- const task = result.tasks![0];
117
- return task.taskArn;
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(jobDetailsFileName, jobContext, jobManifest, executionToStart);
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 { PARTITION_KEYS } from '../../constants/Repository.constants';
27
+ import { ENTITY_TYPES } from '../../constants/Repository.constants';
30
28
 
31
- // Create a new container tsoa can use
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: PARTITION_KEYS.manifest,
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: PARTITION_KEYS.manifest,
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
- const sk = `${name}-${version}`;
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
- const sk = `${name}-${version}`;
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, expect.any(Object));
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, this.logger);
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, mockLogger);
69
+ const result = await getAndValidateManifest(mockApiClient, testFolderPath);
79
70
 
80
- expect(ManifestServiceForDev).toHaveBeenCalledWith(mockLogger);
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, mockLogger)).rejects.toThrow(
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, mockLogger)).rejects.toThrow(
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, mockLogger)).rejects.toThrow(
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, mockLogger);
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, mockLogger);
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, mockLogger);
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, mockLogger);
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, mockLogger);
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, mockLogger);
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, mockLogger);
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
- apiClient: AxiosInstance,
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 './JobQueueService/JobQueueService';
4
+ export * from './JobStartQueueService/JobStartQueueService';
5
+ export * from './JobStartQueueService/JobStartQueueService.defs';
5
6
  export * from './JobUploadService/JobUploadService';
@@ -134,8 +134,6 @@ describe('SlackClient singleton methods', () => {
134
134
 
135
135
  beforeEach(() => {
136
136
  mockMessage = faker.lorem.sentence();
137
-
138
- jest.resetAllMocks();
139
137
  });
140
138
 
141
139
  it('should make a POST request to the Slack webhook URL', async () => {
@@ -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(getLogger({ name: '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
@@ -2,3 +2,4 @@ export * from './domains/manifest/ManifestModel';
2
2
  export * from './domains/component';
3
3
  export * from './domains/job';
4
4
  export * from './domains/manifest';
5
+ export * from './utils';
@@ -2,8 +2,12 @@
2
2
  * @license
3
3
  * Copyright Squiz Australia Pty Ltd. All Rights Reserved.
4
4
  */
5
- import { getLogger } from '@squiz/dx-logger-lib';
5
+ import { Logger } from '@squiz/optimization-logger';
6
6
 
7
- export const testLogger = getLogger({
8
- name: 'component-lib-test',
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
+ };