@clairejs/server 3.27.6 → 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 +4 -0
- package/dist/job/AbstractJobScheduler.d.ts +4 -8
- package/dist/job/AbstractJobScheduler.js +10 -81
- package/dist/job/AwsJobScheduler.d.ts +5 -2
- package/dist/job/AwsJobScheduler.js +77 -101
- package/dist/job/LocalJobScheduler.d.ts +4 -3
- package/dist/job/LocalJobScheduler.js +30 -29
- 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 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,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
|
|
@@ -118,7 +45,7 @@ export class AbstractJobScheduler {
|
|
|
118
45
|
}
|
|
119
46
|
//-- run job
|
|
120
47
|
const allHandlers = await this.getCurrentJobHandlers();
|
|
121
|
-
const jobHandler = allHandlers.find((j) => j.
|
|
48
|
+
const jobHandler = allHandlers.find((j) => j.jobIdentifier === job.jobName);
|
|
122
49
|
const tx = await this.db.createTransaction();
|
|
123
50
|
if (!jobHandler) {
|
|
124
51
|
this.logger.info(`Disable job with id: ${job.id} as handler is not found`);
|
|
@@ -130,7 +57,10 @@ export class AbstractJobScheduler {
|
|
|
130
57
|
execCount: (job.execCount || 0) + 1,
|
|
131
58
|
};
|
|
132
59
|
try {
|
|
60
|
+
this.logger.debug(`Calling job handler for ${job.id}`);
|
|
133
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));
|
|
134
64
|
//-- job run success, update
|
|
135
65
|
update.lastSuccessAt = Date.now();
|
|
136
66
|
//-- reset retry count if this is cron job
|
|
@@ -144,15 +74,14 @@ export class AbstractJobScheduler {
|
|
|
144
74
|
}
|
|
145
75
|
}
|
|
146
76
|
catch (err) {
|
|
77
|
+
this.logger.error(`Job handler for: ${job.id} failed`, err);
|
|
147
78
|
update.failCount = (job.failCount || 0) + 1;
|
|
148
79
|
update.lastError = String(err);
|
|
149
80
|
//-- job run error, check retry
|
|
150
81
|
if (job.retryOnFail) {
|
|
151
82
|
if ((job.retryCount || 0) < (job.maxRetry || 1)) {
|
|
152
|
-
//-- retry by reschedule the job
|
|
153
83
|
update.retryCount = (job.retryCount || 0) + 1;
|
|
154
|
-
|
|
155
|
-
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));
|
|
156
85
|
}
|
|
157
86
|
else {
|
|
158
87
|
if (job.cron) {
|
|
@@ -15,8 +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 getJobUniqueName;
|
|
18
19
|
private getOneTimeExpression;
|
|
19
|
-
|
|
20
|
-
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>;
|
|
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";
|
|
@@ -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 || 1) - 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(job) {
|
|
94
|
+
return await this.removeJobFromScheduler(job.id);
|
|
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: 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}`);
|
|
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(job: AbstractJob): Promise<void>;
|
|
58
|
+
protected registerJob(job: AbstractJob): Promise<void>;
|
|
58
59
|
protected cancelJob(jobId: string): Promise<void>;
|
|
59
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,41 +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
|
|
155
|
+
async cleanupJob(job) {
|
|
156
|
+
this.jobHolder[job.id] = undefined;
|
|
157
|
+
}
|
|
158
|
+
async registerJob(job) {
|
|
158
159
|
if (this.isActive) {
|
|
159
160
|
//-- case each job type
|
|
160
|
-
if (
|
|
161
|
+
if (job.at) {
|
|
161
162
|
//-- use the lib
|
|
162
163
|
const timeout = setTimeout(() => {
|
|
163
|
-
this.executeJob(
|
|
164
|
-
}, new Date(
|
|
165
|
-
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 };
|
|
166
167
|
}
|
|
167
|
-
else if (
|
|
168
|
-
const
|
|
169
|
-
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));
|
|
170
171
|
});
|
|
171
|
-
this.jobHolder[
|
|
172
|
+
this.jobHolder[job.id] = { jobCanceler: () => scheduledJob.cancel(), job };
|
|
172
173
|
}
|
|
173
174
|
else {
|
|
174
|
-
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${
|
|
175
|
+
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${job.jobName}`);
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
else {
|
|
178
179
|
//-- get unique message id
|
|
179
|
-
this.sendJob(CommunicationMessage.SCHEDULE_JOB,
|
|
180
|
+
this.sendJob(CommunicationMessage.SCHEDULE_JOB, job);
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
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;
|