@clairejs/server 3.27.6 → 3.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/job/AbstractJobScheduler.d.ts +5 -9
- package/dist/job/AbstractJobScheduler.js +22 -90
- package/dist/job/AwsJobScheduler.d.ts +6 -3
- package/dist/job/AwsJobScheduler.js +78 -102
- package/dist/job/LocalJobScheduler.d.ts +4 -3
- package/dist/job/LocalJobScheduler.js +45 -39
- package/dist/job/decorators.d.ts +1 -3
- package/dist/job/decorators.js +3 -23
- package/dist/job/interfaces.d.ts +4 -18
- package/dist/system/LambdaWrapper.js +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,23 +13,19 @@ export declare abstract class AbstractJobScheduler {
|
|
|
13
13
|
private _jobs;
|
|
14
14
|
constructor(logger: AbstractLogger, db: ITransactionFactory, jobRepo: AbstractJobRepository);
|
|
15
15
|
protected getCurrentJobHandlers(): Promise<JobHandlerMetadata[]>;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Sync all jobs to running state. This should be called only at init time.
|
|
19
|
-
*/
|
|
20
|
-
syncJobs(): Promise<void>;
|
|
16
|
+
scheduleJob(payload: JobSchedulePayload): Promise<string>;
|
|
21
17
|
/**
|
|
22
18
|
* Return unique job id which can then be used to cancel the job
|
|
23
19
|
* @param payload the necessary info to launch the job
|
|
24
20
|
*/
|
|
25
|
-
protected abstract
|
|
26
|
-
scheduleJob({ id, ...payload }: JobSchedulePayload): Promise<string>;
|
|
21
|
+
protected abstract registerJob(payload: AbstractJob): Promise<void>;
|
|
27
22
|
/**
|
|
28
23
|
* Cancel the scheduled job and prevent if from running in the future
|
|
29
24
|
* @param id The job id returned from scheduleJobAt function
|
|
30
25
|
*/
|
|
31
26
|
protected abstract cancelJob(id: string): Promise<void>;
|
|
32
|
-
protected retryJob(
|
|
27
|
+
protected abstract retryJob(job: AbstractJob, at: number): Promise<void>;
|
|
28
|
+
protected abstract cleanupJob(id: string): Promise<void>;
|
|
33
29
|
/**
|
|
34
30
|
* Remove the scheduled job and prevent if from running in the future
|
|
35
31
|
* @param id The job id returned from scheduleJobAt function
|
|
@@ -39,6 +35,6 @@ export declare abstract class AbstractJobScheduler {
|
|
|
39
35
|
* Execute the scheduled job
|
|
40
36
|
* @param job The schedled job info to execute
|
|
41
37
|
*/
|
|
42
|
-
protected executeJob(
|
|
38
|
+
protected executeJob(jobId: string): Promise<void>;
|
|
43
39
|
}
|
|
44
40
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getServiceProvider } from "@clairejs/core";
|
|
2
2
|
import { AbstractJobController } from "./AbstractJobController";
|
|
3
3
|
export class AbstractJobScheduler {
|
|
4
4
|
logger;
|
|
@@ -22,84 +22,11 @@ export class AbstractJobScheduler {
|
|
|
22
22
|
}
|
|
23
23
|
return this._jobs;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
async syncJobs() {
|
|
29
|
-
//-- check jobs
|
|
30
|
-
const scheduledJobs = await this.getScheduledJobs();
|
|
31
|
-
const allHandlers = await this.getCurrentJobHandlers();
|
|
32
|
-
const tx = await this.db.createTransaction();
|
|
33
|
-
//-- remove job that no more exist
|
|
34
|
-
const nomoreExistJobs = scheduledJobs.filter((job) => !allHandlers.find((handler) => handler.jobName === job.jobName));
|
|
35
|
-
for (const job of nomoreExistJobs) {
|
|
36
|
-
this.logger.info(`Removing stale job: ${job.jobName} of id: ${job.id}`);
|
|
37
|
-
await this.disableJob(job.id, tx);
|
|
38
|
-
}
|
|
39
|
-
if (nomoreExistJobs.length) {
|
|
40
|
-
this.logger.info(`Cleaned up: ${nomoreExistJobs.length} stale jobs`);
|
|
41
|
-
}
|
|
42
|
-
//-- remove scheduled cron jobs that diff the cron expression
|
|
43
|
-
this.logger.debug("Remove scheduled cron jobs");
|
|
44
|
-
const scheduledCronJobs = scheduledJobs.filter((j) => j.cron);
|
|
45
|
-
const updatedScheduledCronJobs = scheduledCronJobs.filter((j) => j.cron && allHandlers.some((job) => job.jobName === j.jobName && job.cron !== j.cron));
|
|
46
|
-
for (const job of updatedScheduledCronJobs) {
|
|
47
|
-
await this.disableJob(job.id, tx);
|
|
48
|
-
}
|
|
49
|
-
await tx.commit();
|
|
50
|
-
//-- reschedule new cron jobs and those which are not synced
|
|
51
|
-
const resyncCronJobs = allHandlers.filter((job) => job.cron &&
|
|
52
|
-
(updatedScheduledCronJobs.some((j) => j.jobName === job.jobName) ||
|
|
53
|
-
!scheduledCronJobs.some((j) => j.jobName === job.jobName)));
|
|
54
|
-
this.logger.debug(`Resync ${resyncCronJobs.length} cron jobs`, resyncCronJobs);
|
|
55
|
-
const allPersistedJobs = await this.jobRepo.getJobs({ _neq: { disabled: true } });
|
|
56
|
-
for (const job of resyncCronJobs) {
|
|
57
|
-
const matchedPersistedJob = allPersistedJobs.find((persistedJob) => persistedJob.jobName === job.jobName && persistedJob.cron === job.cron);
|
|
58
|
-
await this.scheduleJob({
|
|
59
|
-
...matchedPersistedJob,
|
|
60
|
-
...job.retryOptions,
|
|
61
|
-
jobName: job.jobName,
|
|
62
|
-
cron: job.cron,
|
|
63
|
-
at: job.at,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
//-- sync "at" jobs
|
|
67
|
-
const missingScheduledAtJobs = allPersistedJobs.filter((job) => job.at && !scheduledJobs.find((scheduled) => scheduled.id === job.id));
|
|
68
|
-
for (const job of missingScheduledAtJobs) {
|
|
69
|
-
await this.scheduleJob(job);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
async scheduleJob({ id, ...payload }) {
|
|
73
|
-
let jobId = id;
|
|
74
|
-
const tx = await this.db.createTransaction();
|
|
75
|
-
if (!jobId && !payload.cron) {
|
|
76
|
-
jobId = await this.jobRepo.insertJob(payload, tx);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
const job = jobId
|
|
80
|
-
? await this.jobRepo.getJobById(jobId)
|
|
81
|
-
: await this.jobRepo
|
|
82
|
-
.getJobs({ _eq: { jobName: payload.jobName } }, { limit: 1 })
|
|
83
|
-
.then((jobs) => jobs[0]);
|
|
84
|
-
if (!job) {
|
|
85
|
-
jobId = await this.jobRepo.insertJob(payload, tx);
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
jobId = job.id;
|
|
89
|
-
const diff = diffData(job, payload);
|
|
90
|
-
if (diff) {
|
|
91
|
-
await this.jobRepo.updateJobById(job.id, payload, tx);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
await this._scheduleJob({ ...payload, id: jobId });
|
|
96
|
-
await tx.commit();
|
|
25
|
+
async scheduleJob(payload) {
|
|
26
|
+
const jobId = await this.jobRepo.insertJob(payload);
|
|
27
|
+
await this.registerJob({ ...payload, id: jobId });
|
|
97
28
|
return jobId;
|
|
98
29
|
}
|
|
99
|
-
async retryJob(payload) {
|
|
100
|
-
this.logger.debug("Retrying job: ", payload);
|
|
101
|
-
await this._scheduleJob({ ...payload, id: `${payload.id}-retry-${payload.retryCount}` });
|
|
102
|
-
}
|
|
103
30
|
/**
|
|
104
31
|
* Remove the scheduled job and prevent if from running in the future
|
|
105
32
|
* @param id The job id returned from scheduleJobAt function
|
|
@@ -112,13 +39,18 @@ export class AbstractJobScheduler {
|
|
|
112
39
|
* Execute the scheduled job
|
|
113
40
|
* @param job The schedled job info to execute
|
|
114
41
|
*/
|
|
115
|
-
async executeJob(
|
|
42
|
+
async executeJob(jobId) {
|
|
43
|
+
const job = await this.jobRepo.getJobById(jobId);
|
|
44
|
+
if (!job) {
|
|
45
|
+
await this.cancelJob(jobId);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
116
48
|
if (job.disabled) {
|
|
117
49
|
return;
|
|
118
50
|
}
|
|
119
51
|
//-- run job
|
|
120
52
|
const allHandlers = await this.getCurrentJobHandlers();
|
|
121
|
-
const jobHandler = allHandlers.find((j) => j.
|
|
53
|
+
const jobHandler = allHandlers.find((j) => j.jobIdentifier === job.jobName);
|
|
122
54
|
const tx = await this.db.createTransaction();
|
|
123
55
|
if (!jobHandler) {
|
|
124
56
|
this.logger.info(`Disable job with id: ${job.id} as handler is not found`);
|
|
@@ -130,7 +62,10 @@ export class AbstractJobScheduler {
|
|
|
130
62
|
execCount: (job.execCount || 0) + 1,
|
|
131
63
|
};
|
|
132
64
|
try {
|
|
65
|
+
this.logger.debug(`Calling job handler for ${job.id}`);
|
|
133
66
|
await jobHandler.handlerFn(job);
|
|
67
|
+
this.logger.debug(`Job handler for ${job.id} completed`);
|
|
68
|
+
await this.cleanupJob(job.id).catch((err) => this.logger.error(`Failed to cleanup job: ${job.id}`, err));
|
|
134
69
|
//-- job run success, update
|
|
135
70
|
update.lastSuccessAt = Date.now();
|
|
136
71
|
//-- reset retry count if this is cron job
|
|
@@ -144,21 +79,19 @@ export class AbstractJobScheduler {
|
|
|
144
79
|
}
|
|
145
80
|
}
|
|
146
81
|
catch (err) {
|
|
82
|
+
this.logger.error(`Job handler for: ${job.id} failed`, err);
|
|
147
83
|
update.failCount = (job.failCount || 0) + 1;
|
|
148
84
|
update.lastError = String(err);
|
|
149
85
|
//-- job run error, check retry
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
86
|
+
const maxRetry = job.maxRetry || 0;
|
|
87
|
+
if (maxRetry) {
|
|
88
|
+
const currentRetryCount = job.retryCount || 0;
|
|
89
|
+
if (currentRetryCount < maxRetry) {
|
|
90
|
+
await this.retryJob(job, Date.now() + Math.max(job.retryDelayMs || 0, 60000));
|
|
91
|
+
update.retryCount = currentRetryCount + 1;
|
|
156
92
|
}
|
|
157
93
|
else {
|
|
158
|
-
if (job.
|
|
159
|
-
update.at = undefined;
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
94
|
+
if (job.at) {
|
|
162
95
|
update.disabled = true;
|
|
163
96
|
await this.cancelJob(job.id);
|
|
164
97
|
}
|
|
@@ -166,7 +99,6 @@ export class AbstractJobScheduler {
|
|
|
166
99
|
}
|
|
167
100
|
}
|
|
168
101
|
await this.jobRepo.updateJobById(job.id, update, tx);
|
|
169
|
-
Object.assign(job, update);
|
|
170
102
|
}
|
|
171
103
|
await tx.commit();
|
|
172
104
|
}
|
|
@@ -13,10 +13,13 @@ export declare class AwsJobScheduler extends AbstractJobScheduler {
|
|
|
13
13
|
protected readonly jobNamespace: string;
|
|
14
14
|
protected readonly scheduler: SchedulerClient;
|
|
15
15
|
constructor(logger: AbstractLogger, db: AbstractDbAdapter, jobRepo: AbstractJobRepository, apiLambdaFunctionArn: string, apiLambdaFunctionRoleArn: string, jobNamespace: string);
|
|
16
|
-
handleCron(jobInfo: AbstractJob): Promise<void>;
|
|
16
|
+
handleCron(jobInfo: Pick<AbstractJob, "id">): Promise<void>;
|
|
17
17
|
private convertCronToSchedulerExpression;
|
|
18
|
+
private getJobUniqueName;
|
|
18
19
|
private getOneTimeExpression;
|
|
19
|
-
|
|
20
|
-
protected
|
|
20
|
+
private removeJobFromScheduler;
|
|
21
|
+
protected retryJob(job: AbstractJob, at: number): Promise<void>;
|
|
22
|
+
protected cleanupJob(jobId: string): Promise<void>;
|
|
23
|
+
protected registerJob(job: AbstractJob): Promise<void>;
|
|
21
24
|
cancelJob(jobId: string): Promise<void>;
|
|
22
25
|
}
|
|
@@ -9,13 +9,12 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
};
|
|
10
10
|
import { AbstractLogger, Errors, LogContext } from "@clairejs/core";
|
|
11
11
|
import { AbstractDbAdapter } from "@clairejs/orm";
|
|
12
|
-
import { SchedulerClient, CreateScheduleCommand, DeleteScheduleCommand, GetScheduleCommand,
|
|
12
|
+
import { SchedulerClient, CreateScheduleCommand, DeleteScheduleCommand, GetScheduleCommand, ScheduleState, UpdateScheduleCommand, } from "@aws-sdk/client-scheduler";
|
|
13
13
|
import { AbstractJobScheduler } from "./AbstractJobScheduler";
|
|
14
14
|
import { AbstractJobRepository } from "./AbstractJobRepository";
|
|
15
15
|
/**
|
|
16
16
|
* AWS Scheduler API permissions required:
|
|
17
|
-
* scheduler:CreateSchedule, scheduler:DeleteSchedule, scheduler:GetSchedule,
|
|
18
|
-
* scheduler:ListSchedules, scheduler:UpdateSchedule
|
|
17
|
+
* scheduler:CreateSchedule, scheduler:DeleteSchedule, scheduler:GetSchedule, scheduler:UpdateSchedule
|
|
19
18
|
* Also update Lambda function to allow function invocation from scheduler.amazonaws.com.
|
|
20
19
|
*/
|
|
21
20
|
const schedulerGroupName = "default";
|
|
@@ -38,7 +37,7 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
|
|
|
38
37
|
}
|
|
39
38
|
async handleCron(jobInfo) {
|
|
40
39
|
this.logger.debug(`Handle cron`, jobInfo);
|
|
41
|
-
await this.executeJob(jobInfo);
|
|
40
|
+
await this.executeJob(jobInfo.id);
|
|
42
41
|
}
|
|
43
42
|
convertCronToSchedulerExpression(cron) {
|
|
44
43
|
// AWS Scheduler uses a different cron format than standard cron
|
|
@@ -56,118 +55,95 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
|
|
|
56
55
|
}
|
|
57
56
|
return parts.join(" ");
|
|
58
57
|
}
|
|
58
|
+
getJobUniqueName(jobId) {
|
|
59
|
+
return `${this.jobNamespace}${jobId}`;
|
|
60
|
+
}
|
|
59
61
|
getOneTimeExpression(at) {
|
|
60
62
|
const date = new Date(at);
|
|
61
|
-
return `at(${date.toISOString().replace(/\.\d{3}Z$/,
|
|
63
|
+
return `at(${date.toISOString().replace(/\.\d{3}Z$/, "")})`;
|
|
64
|
+
}
|
|
65
|
+
async removeJobFromScheduler(jobId) {
|
|
66
|
+
const jobName = this.getJobUniqueName(jobId);
|
|
67
|
+
const deleteCommand = new DeleteScheduleCommand({
|
|
68
|
+
Name: jobName,
|
|
69
|
+
GroupName: schedulerGroupName,
|
|
70
|
+
});
|
|
71
|
+
await this.scheduler.send(deleteCommand);
|
|
62
72
|
}
|
|
63
|
-
async
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
async retryJob(job, at) {
|
|
74
|
+
const jobName = this.getJobUniqueName(job.id);
|
|
75
|
+
if (job.at) {
|
|
76
|
+
//-- update the job
|
|
77
|
+
const getCommand = new GetScheduleCommand({
|
|
78
|
+
Name: jobName,
|
|
66
79
|
GroupName: schedulerGroupName,
|
|
67
|
-
NamePrefix: this.jobNamespace,
|
|
68
80
|
});
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
GroupName: schedulerGroupName,
|
|
76
|
-
});
|
|
77
|
-
const scheduleDetails = await this.scheduler.send(getCommand);
|
|
78
|
-
if (!scheduleDetails.Target?.Arn || !scheduleDetails.Target?.Input) {
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
const jobInfo = JSON.parse(scheduleDetails.Target.Input).requestContext.cronScheduler.data;
|
|
82
|
-
return jobInfo;
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
this.logger.warn(`Failed to get schedule details for ${scheduleName}`, error);
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
}));
|
|
89
|
-
return allJobs.flatMap((job) => (job ? [job] : []));
|
|
81
|
+
const scheduledJob = await this.scheduler.send(getCommand);
|
|
82
|
+
const updateCommand = new UpdateScheduleCommand({
|
|
83
|
+
...scheduledJob,
|
|
84
|
+
ScheduleExpression: this.getOneTimeExpression(at),
|
|
85
|
+
});
|
|
86
|
+
await this.scheduler.send(updateCommand);
|
|
90
87
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
else {
|
|
89
|
+
//-- schedule a new one-time job to re-execute this cron with minus one maxRetry
|
|
90
|
+
await this.scheduleJob({ ...job, at, maxRetry: (job.maxRetry || 0) - 1 });
|
|
94
91
|
}
|
|
95
92
|
}
|
|
96
|
-
async
|
|
97
|
-
this.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
else {
|
|
120
|
-
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${jobInfo.jobName}`);
|
|
121
|
-
}
|
|
122
|
-
this.logger.debug("Schedule expression", scheduleExpression);
|
|
123
|
-
const scheduleParams = {
|
|
124
|
-
Name: jobId,
|
|
125
|
-
GroupName: schedulerGroupName,
|
|
126
|
-
Description: `${jobInfo.jobName} - ${scheduleExpression}`,
|
|
127
|
-
ScheduleExpression: scheduleExpression,
|
|
128
|
-
FlexibleTimeWindow: flexibleTimeWindow,
|
|
129
|
-
Target: {
|
|
130
|
-
Arn: this.apiLambdaFunctionArn,
|
|
131
|
-
RoleArn: this.apiLambdaFunctionRoleArn,
|
|
132
|
-
Input: JSON.stringify({
|
|
133
|
-
requestContext: {
|
|
134
|
-
cronScheduler: {
|
|
135
|
-
data: { ...jobInfo, jobId },
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
}),
|
|
139
|
-
},
|
|
140
|
-
State: ScheduleState.ENABLED,
|
|
141
|
-
ActionAfterCompletion: ActionAfterCompletion.DELETE,
|
|
93
|
+
async cleanupJob(jobId) {
|
|
94
|
+
return await this.removeJobFromScheduler(jobId);
|
|
95
|
+
}
|
|
96
|
+
async registerJob(job) {
|
|
97
|
+
this.logger.debug("Scheduling job: ", job);
|
|
98
|
+
const jobName = this.getJobUniqueName(job.id);
|
|
99
|
+
let scheduleExpression;
|
|
100
|
+
let flexibleTimeWindow;
|
|
101
|
+
if (job.at) {
|
|
102
|
+
scheduleExpression = this.getOneTimeExpression(job.at);
|
|
103
|
+
// Add flexible time window for one-time jobs to handle slight delays
|
|
104
|
+
flexibleTimeWindow = {
|
|
105
|
+
Mode: "OFF",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
else if (job.cron) {
|
|
109
|
+
// For recurring jobs, use cron() expression
|
|
110
|
+
const schedulerCron = this.convertCronToSchedulerExpression(job.cron);
|
|
111
|
+
scheduleExpression = `cron(${schedulerCron})`;
|
|
112
|
+
// Add flexible time window for recurring jobs
|
|
113
|
+
flexibleTimeWindow = {
|
|
114
|
+
Mode: "FLEXIBLE",
|
|
115
|
+
MaximumWindowInMinutes: 5, // Allow 5-minute window for execution
|
|
142
116
|
};
|
|
143
|
-
try {
|
|
144
|
-
const createCommand = new CreateScheduleCommand(scheduleParams);
|
|
145
|
-
await this.scheduler.send(createCommand);
|
|
146
|
-
this.logger.debug(`Successfully created schedule: ${jobId}`);
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
this.logger.error(`Failed to create schedule: ${jobId}`, error);
|
|
150
|
-
throw error;
|
|
151
|
-
}
|
|
152
117
|
}
|
|
153
118
|
else {
|
|
154
|
-
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${
|
|
119
|
+
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${job.jobName}`);
|
|
155
120
|
}
|
|
121
|
+
this.logger.debug("Schedule expression", scheduleExpression);
|
|
122
|
+
const scheduleParams = {
|
|
123
|
+
Name: jobName,
|
|
124
|
+
GroupName: schedulerGroupName,
|
|
125
|
+
Description: `${job.jobName} - ${scheduleExpression}`,
|
|
126
|
+
ScheduleExpression: scheduleExpression,
|
|
127
|
+
FlexibleTimeWindow: flexibleTimeWindow,
|
|
128
|
+
Target: {
|
|
129
|
+
Arn: this.apiLambdaFunctionArn,
|
|
130
|
+
RoleArn: this.apiLambdaFunctionRoleArn,
|
|
131
|
+
Input: JSON.stringify({
|
|
132
|
+
requestContext: {
|
|
133
|
+
cronScheduler: {
|
|
134
|
+
data: { id: job.id },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
},
|
|
139
|
+
State: ScheduleState.ENABLED,
|
|
140
|
+
};
|
|
141
|
+
const createCommand = new CreateScheduleCommand(scheduleParams);
|
|
142
|
+
await this.scheduler.send(createCommand);
|
|
143
|
+
this.logger.debug(`Successfully created schedule: ${jobName}`);
|
|
156
144
|
}
|
|
157
145
|
async cancelJob(jobId) {
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
const deleteCommand = new DeleteScheduleCommand({
|
|
161
|
-
Name: scheduleName,
|
|
162
|
-
GroupName: schedulerGroupName,
|
|
163
|
-
});
|
|
164
|
-
await this.scheduler.send(deleteCommand);
|
|
165
|
-
this.logger.debug(`Successfully deleted schedule: ${scheduleName}`);
|
|
166
|
-
}
|
|
167
|
-
catch (error) {
|
|
168
|
-
this.logger.error(`Failed to delete schedule: ${scheduleName}`, error);
|
|
169
|
-
throw error;
|
|
170
|
-
}
|
|
146
|
+
await this.removeJobFromScheduler(jobId);
|
|
171
147
|
}
|
|
172
148
|
};
|
|
173
149
|
AwsJobScheduler = __decorate([
|
|
@@ -50,10 +50,11 @@ export declare class LocalJobScheduler extends AbstractJobScheduler implements I
|
|
|
50
50
|
private sendJob;
|
|
51
51
|
private processMessage;
|
|
52
52
|
private extendMutexKey;
|
|
53
|
+
private loadAndActivateJobs;
|
|
53
54
|
init(): Promise<void>;
|
|
54
55
|
exit(): void;
|
|
55
|
-
protected
|
|
56
|
-
|
|
57
|
-
protected
|
|
56
|
+
protected retryJob(job: AbstractJob, at: number): Promise<void>;
|
|
57
|
+
protected cleanupJob(jobId: string): Promise<void>;
|
|
58
|
+
protected registerJob(job: AbstractJob): Promise<void>;
|
|
58
59
|
protected cancelJob(jobId: string): Promise<void>;
|
|
59
60
|
}
|
|
@@ -18,7 +18,7 @@ var CommunicationMessage;
|
|
|
18
18
|
(function (CommunicationMessage) {
|
|
19
19
|
CommunicationMessage["SCHEDULE_JOB"] = "SCHEDULE_JOB";
|
|
20
20
|
CommunicationMessage["REMOVE_JOB"] = "REMOVE_JOB";
|
|
21
|
-
CommunicationMessage["
|
|
21
|
+
CommunicationMessage["CLEANUP_JOB"] = "CLEANUP_JOB";
|
|
22
22
|
})(CommunicationMessage || (CommunicationMessage = {}));
|
|
23
23
|
let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
24
24
|
logger;
|
|
@@ -66,30 +66,26 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
66
66
|
this.subscribeClient.publish(this.multiClientChannel, JSON.stringify({ type, data }));
|
|
67
67
|
}
|
|
68
68
|
async processMessage(type, data) {
|
|
69
|
+
if (!this.isActive) {
|
|
70
|
+
//-- not active scheduler, ignore
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
69
73
|
switch (type) {
|
|
70
|
-
case CommunicationMessage.
|
|
71
|
-
if (!this.isActive) {
|
|
72
|
-
//-- not active scheduler, ignore
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
await this.syncJobs();
|
|
76
|
-
break;
|
|
77
|
-
case CommunicationMessage.SCHEDULE_JOB:
|
|
78
|
-
if (!this.isActive) {
|
|
79
|
-
//-- not active scheduler, ignore
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
74
|
+
case CommunicationMessage.SCHEDULE_JOB: {
|
|
82
75
|
const jobInfo = data;
|
|
83
|
-
await this.
|
|
76
|
+
await this.registerJob(jobInfo);
|
|
84
77
|
break;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
}
|
|
79
|
+
case CommunicationMessage.REMOVE_JOB: {
|
|
80
|
+
const jobId = data;
|
|
81
|
+
await this.cancelJob(jobId);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case CommunicationMessage.CLEANUP_JOB: {
|
|
90
85
|
const jobId = data;
|
|
91
|
-
await this.
|
|
86
|
+
await this.cleanupJob(jobId);
|
|
92
87
|
break;
|
|
88
|
+
}
|
|
93
89
|
default:
|
|
94
90
|
this.logger.error(`Not recognize message type ${type}`);
|
|
95
91
|
break;
|
|
@@ -103,6 +99,13 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
103
99
|
await this.redisClient.setex(this.holdMutexKey, this.keyRetentionDurationSeconds, 1);
|
|
104
100
|
this.logger.debug("Scheduler extends mutex key");
|
|
105
101
|
}
|
|
102
|
+
//-- load and activate jobs
|
|
103
|
+
async loadAndActivateJobs() {
|
|
104
|
+
const jobs = await this.jobRepo.getJobs({ _neq: { disabled: true } });
|
|
105
|
+
for (const job of jobs) {
|
|
106
|
+
await this.registerJob(job);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
106
109
|
async init() {
|
|
107
110
|
this.logger.debug("LocalJobScheduler init");
|
|
108
111
|
//-- subscribe to multi client channel
|
|
@@ -128,6 +131,7 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
128
131
|
this.mutexHoldInterval = setInterval(() => {
|
|
129
132
|
this.extendMutexKey();
|
|
130
133
|
}, Math.trunc((this.keyRetentionDurationSeconds * 1000) / 2) + 1);
|
|
134
|
+
await this.loadAndActivateJobs();
|
|
131
135
|
}
|
|
132
136
|
catch (err) {
|
|
133
137
|
this.logger.info("Failed to lock mutex key, ignore", err);
|
|
@@ -142,41 +146,43 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
142
146
|
}
|
|
143
147
|
this.logger.debug("LocalJobScheduler exit");
|
|
144
148
|
}
|
|
145
|
-
async
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (this.isActive) {
|
|
150
|
-
await super.syncJobs();
|
|
149
|
+
async retryJob(job, at) {
|
|
150
|
+
if (job.at) {
|
|
151
|
+
//-- just register the job at new timestamp
|
|
152
|
+
await this.registerJob({ ...job, at });
|
|
151
153
|
}
|
|
152
154
|
else {
|
|
153
|
-
this
|
|
155
|
+
//-- schedule a new one-time job to re-execute this cron with minus one maxRetry
|
|
156
|
+
await this.scheduleJob({ ...job, at, maxRetry: (job.maxRetry || 0) - 1 });
|
|
154
157
|
}
|
|
155
|
-
return;
|
|
156
158
|
}
|
|
157
|
-
async
|
|
159
|
+
async cleanupJob(jobId) {
|
|
160
|
+
this.jobHolder[jobId] = undefined;
|
|
161
|
+
this.sendJob(CommunicationMessage.CLEANUP_JOB, jobId);
|
|
162
|
+
}
|
|
163
|
+
async registerJob(job) {
|
|
158
164
|
if (this.isActive) {
|
|
159
165
|
//-- case each job type
|
|
160
|
-
if (
|
|
166
|
+
if (job.at) {
|
|
161
167
|
//-- use the lib
|
|
162
168
|
const timeout = setTimeout(() => {
|
|
163
|
-
this.executeJob(
|
|
164
|
-
}, new Date(
|
|
165
|
-
this.jobHolder[
|
|
169
|
+
this.executeJob(job.id).catch((err) => this.logger.error(`Error execute job ${job.jobName} with id: ${job.id}`, err));
|
|
170
|
+
}, new Date(job.at).getTime() - Date.now());
|
|
171
|
+
this.jobHolder[job.id] = { jobCanceler: () => clearTimeout(timeout), job };
|
|
166
172
|
}
|
|
167
|
-
else if (
|
|
168
|
-
const
|
|
169
|
-
this.executeJob(
|
|
173
|
+
else if (job.cron) {
|
|
174
|
+
const scheduledJob = scheduler.scheduleJob(job.cron, () => {
|
|
175
|
+
this.executeJob(job.id).catch((err) => this.logger.error(`Error execute job ${job.jobName} with id: ${job.id}`, err));
|
|
170
176
|
});
|
|
171
|
-
this.jobHolder[
|
|
177
|
+
this.jobHolder[job.id] = { jobCanceler: () => scheduledJob.cancel(), job };
|
|
172
178
|
}
|
|
173
179
|
else {
|
|
174
|
-
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${
|
|
180
|
+
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${job.jobName}`);
|
|
175
181
|
}
|
|
176
182
|
}
|
|
177
183
|
else {
|
|
178
184
|
//-- get unique message id
|
|
179
|
-
this.sendJob(CommunicationMessage.SCHEDULE_JOB,
|
|
185
|
+
this.sendJob(CommunicationMessage.SCHEDULE_JOB, job);
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
188
|
async cancelJob(jobId) {
|
package/dist/job/decorators.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { JobRetryOptions } from "./interfaces";
|
|
2
1
|
import { AbstractJobController } from "./AbstractJobController";
|
|
3
2
|
import { JobHandlerFn } from "./AbstractJobScheduler";
|
|
4
3
|
type Descriptor<T> = Omit<TypedPropertyDescriptor<T>, "set">;
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const CustomJob: (jobName: string) => <T extends AbstractJobController>(prototype: T, propertyKey: keyof T, _descriptor: Descriptor<JobHandlerFn>) => void;
|
|
4
|
+
export declare const Job: (jobIdentifier: string) => <T extends AbstractJobController>(prototype: T, propertyKey: keyof T, _descriptor: Descriptor<JobHandlerFn>) => void;
|
|
7
5
|
export {};
|
package/dist/job/decorators.js
CHANGED
|
@@ -1,31 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const
|
|
3
|
-
const metadata = initObjectMetadata(prototype);
|
|
4
|
-
if (!metadata.jobs) {
|
|
5
|
-
metadata.jobs = [];
|
|
6
|
-
}
|
|
7
|
-
//-- validate cron
|
|
8
|
-
if (cron.split(" ").length !== 5) {
|
|
9
|
-
throw Errors.SYSTEM_ERROR("Invalid cron expression, expect minute / hour / day / month / day-of-week");
|
|
10
|
-
}
|
|
11
|
-
metadata.jobs.push({
|
|
12
|
-
jobName,
|
|
13
|
-
retryOptions,
|
|
14
|
-
cron,
|
|
15
|
-
handlerName: propertyKey,
|
|
16
|
-
});
|
|
17
|
-
};
|
|
18
|
-
export const CustomJob = (
|
|
19
|
-
/**
|
|
20
|
-
* Unique name of job
|
|
21
|
-
*/
|
|
22
|
-
jobName) => (prototype, propertyKey, _descriptor) => {
|
|
1
|
+
import { initObjectMetadata } from "@clairejs/core";
|
|
2
|
+
export const Job = (jobIdentifier) => (prototype, propertyKey, _descriptor) => {
|
|
23
3
|
const metadata = initObjectMetadata(prototype);
|
|
24
4
|
if (!metadata.jobs) {
|
|
25
5
|
metadata.jobs = [];
|
|
26
6
|
}
|
|
27
7
|
metadata.jobs.push({
|
|
28
|
-
|
|
8
|
+
jobIdentifier,
|
|
29
9
|
handlerName: propertyKey,
|
|
30
10
|
});
|
|
31
11
|
};
|
package/dist/job/interfaces.d.ts
CHANGED
|
@@ -1,37 +1,23 @@
|
|
|
1
1
|
import { ObjectMetadata } from "@clairejs/core";
|
|
2
2
|
export declare const CRON_REQUEST_METHOD = "cron";
|
|
3
3
|
export interface JobRetryOptions {
|
|
4
|
-
retryOnFail?: boolean;
|
|
5
4
|
maxRetry?: number;
|
|
6
5
|
retryDelayMs?: number;
|
|
7
6
|
}
|
|
8
7
|
export interface JobInfoMetadata {
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
9
|
+
* Name of the handler function
|
|
11
10
|
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Retry options
|
|
15
|
-
*/
|
|
16
|
-
retryOptions?: JobRetryOptions;
|
|
17
|
-
/**
|
|
18
|
-
* Run with cron expression, does not support seconds precision
|
|
19
|
-
*/
|
|
20
|
-
cron?: string;
|
|
21
|
-
/**
|
|
22
|
-
* Run at specific timestamp, does not support seconds precision
|
|
23
|
-
*/
|
|
24
|
-
at?: number;
|
|
11
|
+
handlerName: string;
|
|
25
12
|
/**
|
|
26
|
-
*
|
|
13
|
+
* Unique identifier of the job
|
|
27
14
|
*/
|
|
28
|
-
|
|
15
|
+
jobIdentifier: string;
|
|
29
16
|
}
|
|
30
17
|
export interface JobControllerMetadata extends ObjectMetadata {
|
|
31
18
|
jobs?: JobInfoMetadata[];
|
|
32
19
|
}
|
|
33
20
|
export interface JobSchedulePayload extends JobRetryOptions {
|
|
34
|
-
id?: string;
|
|
35
21
|
jobName: string;
|
|
36
22
|
params?: any;
|
|
37
23
|
at?: number;
|
|
@@ -106,7 +106,6 @@ export class LambdaWrapper {
|
|
|
106
106
|
return toApiGatewayFormat({ code: 200, headers: corsHeaders, cookies: {} });
|
|
107
107
|
}
|
|
108
108
|
if (requestOptions.method === CRON_REQUEST_METHOD) {
|
|
109
|
-
console.log("handle cron", requestOptions.body);
|
|
110
109
|
await this.jobScheduler?.handleCron(requestOptions.body);
|
|
111
110
|
return toApiGatewayFormat({ code: 200, headers: corsHeaders, cookies: {} });
|
|
112
111
|
}
|