@clairejs/server 3.27.4 → 3.28.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/README.md +9 -1
- package/dist/job/AbstractJobScheduler.d.ts +4 -8
- package/dist/job/AbstractJobScheduler.js +10 -77
- package/dist/job/AwsJobScheduler.d.ts +5 -4
- package/dist/job/AwsJobScheduler.js +74 -123
- package/dist/job/LocalJobScheduler.d.ts +4 -4
- package/dist/job/LocalJobScheduler.js +29 -31
- package/dist/job/decorators.d.ts +1 -3
- package/dist/job/decorators.js +3 -23
- package/dist/job/interfaces.d.ts +4 -17
- 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 abstract retryJob(
|
|
27
|
+
protected abstract retryJob(job: AbstractJob, at: number): Promise<void>;
|
|
28
|
+
protected abstract cleanupJob(job: AbstractJob): 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
|
|
@@ -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,78 +22,9 @@ 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
30
|
/**
|
|
@@ -114,7 +45,7 @@ export class AbstractJobScheduler {
|
|
|
114
45
|
}
|
|
115
46
|
//-- run job
|
|
116
47
|
const allHandlers = await this.getCurrentJobHandlers();
|
|
117
|
-
const jobHandler = allHandlers.find((j) => j.
|
|
48
|
+
const jobHandler = allHandlers.find((j) => j.jobIdentifier === job.jobName);
|
|
118
49
|
const tx = await this.db.createTransaction();
|
|
119
50
|
if (!jobHandler) {
|
|
120
51
|
this.logger.info(`Disable job with id: ${job.id} as handler is not found`);
|
|
@@ -126,7 +57,10 @@ export class AbstractJobScheduler {
|
|
|
126
57
|
execCount: (job.execCount || 0) + 1,
|
|
127
58
|
};
|
|
128
59
|
try {
|
|
60
|
+
this.logger.debug(`Calling job handler for ${job.id}`);
|
|
129
61
|
await jobHandler.handlerFn(job);
|
|
62
|
+
this.logger.debug(`Job handler for ${job.id} completed`);
|
|
63
|
+
await this.cleanupJob(job).catch((err) => this.logger.error(`Failed to cleanup job: ${job.id}`, err));
|
|
130
64
|
//-- job run success, update
|
|
131
65
|
update.lastSuccessAt = Date.now();
|
|
132
66
|
//-- reset retry count if this is cron job
|
|
@@ -140,15 +74,14 @@ export class AbstractJobScheduler {
|
|
|
140
74
|
}
|
|
141
75
|
}
|
|
142
76
|
catch (err) {
|
|
77
|
+
this.logger.error(`Job handler for: ${job.id} failed`, err);
|
|
143
78
|
update.failCount = (job.failCount || 0) + 1;
|
|
144
79
|
update.lastError = String(err);
|
|
145
80
|
//-- job run error, check retry
|
|
146
81
|
if (job.retryOnFail) {
|
|
147
82
|
if ((job.retryCount || 0) < (job.maxRetry || 1)) {
|
|
148
|
-
//-- retry by reschedule the job
|
|
149
83
|
update.retryCount = (job.retryCount || 0) + 1;
|
|
150
|
-
|
|
151
|
-
await this.retryJob({ ...job, ...update, at: Date.now() + (retryDelay < 60000 ? 60000 : retryDelay) });
|
|
84
|
+
await this.retryJob({ ...job, ...update }, Date.now() + Math.max(job.retryDelayMs || 0, 60000));
|
|
152
85
|
}
|
|
153
86
|
else {
|
|
154
87
|
if (job.cron) {
|
|
@@ -15,10 +15,11 @@ export declare class AwsJobScheduler extends AbstractJobScheduler {
|
|
|
15
15
|
constructor(logger: AbstractLogger, db: AbstractDbAdapter, jobRepo: AbstractJobRepository, apiLambdaFunctionArn: string, apiLambdaFunctionRoleArn: string, jobNamespace: string);
|
|
16
16
|
handleCron(jobInfo: AbstractJob): Promise<void>;
|
|
17
17
|
private convertCronToSchedulerExpression;
|
|
18
|
-
private
|
|
18
|
+
private getJobUniqueName;
|
|
19
19
|
private getOneTimeExpression;
|
|
20
|
-
|
|
21
|
-
protected retryJob(
|
|
22
|
-
protected
|
|
20
|
+
private removeJobFromScheduler;
|
|
21
|
+
protected retryJob(job: AbstractJob, at: number): Promise<void>;
|
|
22
|
+
protected cleanupJob(job: AbstractJob): Promise<void>;
|
|
23
|
+
protected registerJob(job: AbstractJob): Promise<void>;
|
|
23
24
|
cancelJob(jobId: string): Promise<void>;
|
|
24
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";
|
|
@@ -56,143 +55,95 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
|
|
|
56
55
|
}
|
|
57
56
|
return parts.join(" ");
|
|
58
57
|
}
|
|
59
|
-
|
|
58
|
+
getJobUniqueName(jobId) {
|
|
60
59
|
return `${this.jobNamespace}${jobId}`;
|
|
61
60
|
}
|
|
62
61
|
getOneTimeExpression(at) {
|
|
63
62
|
const date = new Date(at);
|
|
64
|
-
return `at(${date.toISOString().replace(/\.\d{3}Z$/,
|
|
63
|
+
return `at(${date.toISOString().replace(/\.\d{3}Z$/, "")})`;
|
|
65
64
|
}
|
|
66
|
-
async
|
|
67
|
-
|
|
68
|
-
|
|
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);
|
|
72
|
+
}
|
|
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,
|
|
69
79
|
GroupName: schedulerGroupName,
|
|
70
|
-
NamePrefix: this.jobNamespace,
|
|
71
80
|
});
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
GroupName: schedulerGroupName,
|
|
79
|
-
});
|
|
80
|
-
const scheduleDetails = await this.scheduler.send(getCommand);
|
|
81
|
-
if (!scheduleDetails.Target?.Arn || !scheduleDetails.Target?.Input) {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
const jobInfo = JSON.parse(scheduleDetails.Target.Input).requestContext.cronScheduler.data;
|
|
85
|
-
return jobInfo;
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
this.logger.warn(`Failed to get schedule details for ${scheduleName}`, error);
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
}));
|
|
92
|
-
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);
|
|
93
87
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 || 1) - 1 });
|
|
97
91
|
}
|
|
98
92
|
}
|
|
99
|
-
async
|
|
100
|
-
this.
|
|
101
|
-
if (payload.at) {
|
|
102
|
-
if (payload.cron) {
|
|
103
|
-
//- retry a cron job
|
|
104
|
-
await this._scheduleJob({ ...payload, id: `${payload.id}-retry-${payload.retryCount}` });
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
//- retry a one-time job, update the schedule
|
|
108
|
-
const jobName = this.getJobName(payload.id);
|
|
109
|
-
const schedule = await this.scheduler.send(new GetScheduleCommand({
|
|
110
|
-
Name: jobName,
|
|
111
|
-
GroupName: schedulerGroupName,
|
|
112
|
-
}));
|
|
113
|
-
const updateCommand = new UpdateScheduleCommand({
|
|
114
|
-
...schedule,
|
|
115
|
-
ScheduleExpression: this.getOneTimeExpression(payload.at),
|
|
116
|
-
});
|
|
117
|
-
await this.scheduler.send(updateCommand);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
93
|
+
async cleanupJob(job) {
|
|
94
|
+
return await this.removeJobFromScheduler(job.id);
|
|
120
95
|
}
|
|
121
|
-
async
|
|
122
|
-
this.logger.debug("Scheduling job: ",
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
MaximumWindowInMinutes: 5, // Allow 5-minute window for execution
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${jobInfo.jobName}`);
|
|
146
|
-
}
|
|
147
|
-
this.logger.debug("Schedule expression", scheduleExpression);
|
|
148
|
-
const scheduleParams = {
|
|
149
|
-
Name: jobId,
|
|
150
|
-
GroupName: schedulerGroupName,
|
|
151
|
-
Description: `${jobInfo.jobName} - ${scheduleExpression}`,
|
|
152
|
-
ScheduleExpression: scheduleExpression,
|
|
153
|
-
FlexibleTimeWindow: flexibleTimeWindow,
|
|
154
|
-
Target: {
|
|
155
|
-
Arn: this.apiLambdaFunctionArn,
|
|
156
|
-
RoleArn: this.apiLambdaFunctionRoleArn,
|
|
157
|
-
Input: JSON.stringify({
|
|
158
|
-
requestContext: {
|
|
159
|
-
cronScheduler: {
|
|
160
|
-
data: { ...jobInfo, jobId },
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
}),
|
|
164
|
-
},
|
|
165
|
-
State: ScheduleState.ENABLED,
|
|
166
|
-
ActionAfterCompletion: ActionAfterCompletion.DELETE,
|
|
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
|
|
167
116
|
};
|
|
168
|
-
try {
|
|
169
|
-
const createCommand = new CreateScheduleCommand(scheduleParams);
|
|
170
|
-
await this.scheduler.send(createCommand);
|
|
171
|
-
this.logger.debug(`Successfully created schedule: ${jobId}`);
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
this.logger.error(`Failed to create schedule: ${jobId}`, error);
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
177
117
|
}
|
|
178
118
|
else {
|
|
179
|
-
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${
|
|
119
|
+
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${job.jobName}`);
|
|
180
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: job,
|
|
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}`);
|
|
181
144
|
}
|
|
182
145
|
async cancelJob(jobId) {
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
const deleteCommand = new DeleteScheduleCommand({
|
|
186
|
-
Name: scheduleName,
|
|
187
|
-
GroupName: schedulerGroupName,
|
|
188
|
-
});
|
|
189
|
-
await this.scheduler.send(deleteCommand);
|
|
190
|
-
this.logger.debug(`Successfully deleted schedule: ${scheduleName}`);
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
this.logger.error(`Failed to delete schedule: ${scheduleName}`, error);
|
|
194
|
-
throw error;
|
|
195
|
-
}
|
|
146
|
+
await this.removeJobFromScheduler(jobId);
|
|
196
147
|
}
|
|
197
148
|
};
|
|
198
149
|
AwsJobScheduler = __decorate([
|
|
@@ -50,11 +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
|
|
58
|
-
protected _scheduleJob(jobInfo: AbstractJob): Promise<void>;
|
|
56
|
+
protected retryJob(job: AbstractJob, at: number): Promise<void>;
|
|
57
|
+
protected cleanupJob(job: AbstractJob): Promise<void>;
|
|
58
|
+
protected registerJob(job: AbstractJob): Promise<void>;
|
|
59
59
|
protected cancelJob(jobId: string): Promise<void>;
|
|
60
60
|
}
|
|
@@ -18,7 +18,6 @@ var CommunicationMessage;
|
|
|
18
18
|
(function (CommunicationMessage) {
|
|
19
19
|
CommunicationMessage["SCHEDULE_JOB"] = "SCHEDULE_JOB";
|
|
20
20
|
CommunicationMessage["REMOVE_JOB"] = "REMOVE_JOB";
|
|
21
|
-
CommunicationMessage["SYNC_JOB"] = "SYNC_JOB";
|
|
22
21
|
})(CommunicationMessage || (CommunicationMessage = {}));
|
|
23
22
|
let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
24
23
|
logger;
|
|
@@ -67,20 +66,13 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
67
66
|
}
|
|
68
67
|
async processMessage(type, data) {
|
|
69
68
|
switch (type) {
|
|
70
|
-
case CommunicationMessage.SYNC_JOB:
|
|
71
|
-
if (!this.isActive) {
|
|
72
|
-
//-- not active scheduler, ignore
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
await this.syncJobs();
|
|
76
|
-
break;
|
|
77
69
|
case CommunicationMessage.SCHEDULE_JOB:
|
|
78
70
|
if (!this.isActive) {
|
|
79
71
|
//-- not active scheduler, ignore
|
|
80
72
|
return;
|
|
81
73
|
}
|
|
82
74
|
const jobInfo = data;
|
|
83
|
-
await this.
|
|
75
|
+
await this.registerJob(jobInfo);
|
|
84
76
|
break;
|
|
85
77
|
case CommunicationMessage.REMOVE_JOB:
|
|
86
78
|
if (!this.isActive) {
|
|
@@ -88,7 +80,7 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
88
80
|
return;
|
|
89
81
|
}
|
|
90
82
|
const jobId = data;
|
|
91
|
-
await this.
|
|
83
|
+
await this.cancelJob(jobId);
|
|
92
84
|
break;
|
|
93
85
|
default:
|
|
94
86
|
this.logger.error(`Not recognize message type ${type}`);
|
|
@@ -103,6 +95,13 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
103
95
|
await this.redisClient.setex(this.holdMutexKey, this.keyRetentionDurationSeconds, 1);
|
|
104
96
|
this.logger.debug("Scheduler extends mutex key");
|
|
105
97
|
}
|
|
98
|
+
//-- load and activate jobs
|
|
99
|
+
async loadAndActivateJobs() {
|
|
100
|
+
const jobs = await this.jobRepo.getJobs({ _neq: { disabled: true } });
|
|
101
|
+
for (const job of jobs) {
|
|
102
|
+
await this.registerJob(job);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
106
105
|
async init() {
|
|
107
106
|
this.logger.debug("LocalJobScheduler init");
|
|
108
107
|
//-- subscribe to multi client channel
|
|
@@ -128,6 +127,7 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
128
127
|
this.mutexHoldInterval = setInterval(() => {
|
|
129
128
|
this.extendMutexKey();
|
|
130
129
|
}, Math.trunc((this.keyRetentionDurationSeconds * 1000) / 2) + 1);
|
|
130
|
+
await this.loadAndActivateJobs();
|
|
131
131
|
}
|
|
132
132
|
catch (err) {
|
|
133
133
|
this.logger.info("Failed to lock mutex key, ignore", err);
|
|
@@ -142,44 +142,42 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
142
142
|
}
|
|
143
143
|
this.logger.debug("LocalJobScheduler exit");
|
|
144
144
|
}
|
|
145
|
-
async
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (this.isActive) {
|
|
150
|
-
await super.syncJobs();
|
|
145
|
+
async retryJob(job, at) {
|
|
146
|
+
if (job.at) {
|
|
147
|
+
//-- just register the job at new timestamp
|
|
148
|
+
await this.registerJob({ ...job, at });
|
|
151
149
|
}
|
|
152
150
|
else {
|
|
153
|
-
this
|
|
151
|
+
//-- schedule a new one-time job to re-execute this cron with minus one retry
|
|
152
|
+
await this.scheduleJob({ ...job, at, maxRetry: (job.maxRetry || 1) - 1 });
|
|
154
153
|
}
|
|
155
|
-
return;
|
|
156
154
|
}
|
|
157
|
-
async
|
|
158
|
-
|
|
155
|
+
async cleanupJob(job) {
|
|
156
|
+
this.jobHolder[job.id] = undefined;
|
|
159
157
|
}
|
|
160
|
-
async
|
|
158
|
+
async registerJob(job) {
|
|
161
159
|
if (this.isActive) {
|
|
162
160
|
//-- case each job type
|
|
163
|
-
if (
|
|
161
|
+
if (job.at) {
|
|
164
162
|
//-- use the lib
|
|
165
163
|
const timeout = setTimeout(() => {
|
|
166
|
-
this.executeJob(
|
|
167
|
-
}, new Date(
|
|
168
|
-
this.jobHolder[
|
|
164
|
+
this.executeJob(job).catch((err) => this.logger.error(`Error execute job ${job.jobName} with id: ${job.id}`, err));
|
|
165
|
+
}, new Date(job.at).getTime() - Date.now());
|
|
166
|
+
this.jobHolder[job.id] = { jobCanceler: () => clearTimeout(timeout), job };
|
|
169
167
|
}
|
|
170
|
-
else if (
|
|
171
|
-
const
|
|
172
|
-
this.executeJob(
|
|
168
|
+
else if (job.cron) {
|
|
169
|
+
const scheduledJob = scheduler.scheduleJob(job.cron, () => {
|
|
170
|
+
this.executeJob(job).catch((err) => this.logger.error(`Error execute job ${job.jobName} with id: ${job.id}`, err));
|
|
173
171
|
});
|
|
174
|
-
this.jobHolder[
|
|
172
|
+
this.jobHolder[job.id] = { jobCanceler: () => scheduledJob.cancel(), job };
|
|
175
173
|
}
|
|
176
174
|
else {
|
|
177
|
-
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${
|
|
175
|
+
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${job.jobName}`);
|
|
178
176
|
}
|
|
179
177
|
}
|
|
180
178
|
else {
|
|
181
179
|
//-- get unique message id
|
|
182
|
-
this.sendJob(CommunicationMessage.SCHEDULE_JOB,
|
|
180
|
+
this.sendJob(CommunicationMessage.SCHEDULE_JOB, job);
|
|
183
181
|
}
|
|
184
182
|
}
|
|
185
183
|
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
|
@@ -7,31 +7,18 @@ export interface JobRetryOptions {
|
|
|
7
7
|
}
|
|
8
8
|
export interface JobInfoMetadata {
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Name of the handler function
|
|
11
11
|
*/
|
|
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;
|
|
12
|
+
handlerName: string;
|
|
25
13
|
/**
|
|
26
|
-
*
|
|
14
|
+
* Unique identifier of the job
|
|
27
15
|
*/
|
|
28
|
-
|
|
16
|
+
jobIdentifier: string;
|
|
29
17
|
}
|
|
30
18
|
export interface JobControllerMetadata extends ObjectMetadata {
|
|
31
19
|
jobs?: JobInfoMetadata[];
|
|
32
20
|
}
|
|
33
21
|
export interface JobSchedulePayload extends JobRetryOptions {
|
|
34
|
-
id?: string;
|
|
35
22
|
jobName: string;
|
|
36
23
|
params?: any;
|
|
37
24
|
at?: number;
|