@clairejs/server 3.26.1 → 3.27.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 +8 -0
- package/dist/job/AbstractJobScheduler.d.ts +5 -0
- package/dist/job/AbstractJobScheduler.js +2 -0
- package/dist/job/AwsJobScheduler.d.ts +6 -9
- package/dist/job/AwsJobScheduler.js +122 -92
- package/dist/job/LocalJobScheduler.d.ts +1 -0
- package/dist/job/LocalJobScheduler.js +3 -0
- package/dist/system/LambdaWrapper.js +1 -0
- package/dist/system/lamba-request-mapper.js +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -29,6 +29,11 @@ export declare abstract class AbstractJobScheduler {
|
|
|
29
29
|
* @param id The job id returned from scheduleJobAt function
|
|
30
30
|
*/
|
|
31
31
|
protected abstract cancelJob(id: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Clean up the job after it has been executed
|
|
34
|
+
* @param job The job to clean up
|
|
35
|
+
*/
|
|
36
|
+
protected abstract cleanupJob(job: AbstractJob): Promise<void>;
|
|
32
37
|
/**
|
|
33
38
|
* Remove the scheduled job and prevent if from running in the future
|
|
34
39
|
* @param id The job id returned from scheduleJobAt function
|
|
@@ -129,6 +129,8 @@ export class AbstractJobScheduler {
|
|
|
129
129
|
await jobHandler.handlerFn(job);
|
|
130
130
|
//-- job run success, update
|
|
131
131
|
update.lastSuccessAt = Date.now();
|
|
132
|
+
//-- clean up
|
|
133
|
+
await this.cleanupJob(job);
|
|
132
134
|
//-- reset retry count if this is cron job
|
|
133
135
|
if (job.cron) {
|
|
134
136
|
if (job.retryCount) {
|
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import { AbstractLogger } from "@clairejs/core";
|
|
2
2
|
import { AbstractDbAdapter } from "@clairejs/orm";
|
|
3
|
-
import
|
|
3
|
+
import { SchedulerClient } from "@aws-sdk/client-scheduler";
|
|
4
4
|
import { AbstractJob } from "./interfaces";
|
|
5
5
|
import { AbstractJobScheduler } from "./AbstractJobScheduler";
|
|
6
6
|
import { AbstractJobRepository } from "./AbstractJobRepository";
|
|
7
|
-
/**
|
|
8
|
-
* Following EventBrige permissions is required: listRules, putRule, deleteRule, listTargetsByRule, putTargets, removeTargets.
|
|
9
|
-
* Also update Lambda function to alow function invocation from events.amazonaws.com.
|
|
10
|
-
*/
|
|
11
7
|
export declare class AwsJobScheduler extends AbstractJobScheduler {
|
|
12
8
|
protected readonly logger: AbstractLogger;
|
|
13
9
|
protected readonly db: AbstractDbAdapter;
|
|
14
10
|
protected readonly jobRepo: AbstractJobRepository;
|
|
15
11
|
protected readonly apiLambdaFunctionArn: string;
|
|
12
|
+
protected readonly apiLambdaFunctionRoleArn: string;
|
|
16
13
|
protected readonly jobNamespace: string;
|
|
17
|
-
protected readonly
|
|
18
|
-
|
|
19
|
-
constructor(logger: AbstractLogger, db: AbstractDbAdapter, jobRepo: AbstractJobRepository, apiLambdaFunctionArn: string, jobNamespace: string, eventBusName?: string);
|
|
14
|
+
protected readonly scheduler: SchedulerClient;
|
|
15
|
+
constructor(logger: AbstractLogger, db: AbstractDbAdapter, jobRepo: AbstractJobRepository, apiLambdaFunctionArn: string, apiLambdaFunctionRoleArn: string, jobNamespace: string);
|
|
20
16
|
handleCron(jobInfo: AbstractJob): Promise<void>;
|
|
21
|
-
private
|
|
17
|
+
private convertCronToSchedulerExpression;
|
|
18
|
+
protected cleanupJob(job: AbstractJob): Promise<void>;
|
|
22
19
|
protected getScheduledJobs(): Promise<AbstractJob[]>;
|
|
23
20
|
protected _scheduleJob(jobInfo: AbstractJob): Promise<void>;
|
|
24
21
|
cancelJob(jobId: string): Promise<void>;
|
|
@@ -9,143 +9,173 @@ 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
|
|
13
|
-
import aws from "aws-sdk";
|
|
12
|
+
import { SchedulerClient, CreateScheduleCommand, DeleteScheduleCommand, GetScheduleCommand, ListSchedulesCommand, ScheduleState } from "@aws-sdk/client-scheduler";
|
|
14
13
|
import { AbstractJobScheduler } from "./AbstractJobScheduler";
|
|
15
14
|
import { AbstractJobRepository } from "./AbstractJobRepository";
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* AWS Scheduler API permissions required:
|
|
17
|
+
* scheduler:CreateSchedule, scheduler:DeleteSchedule, scheduler:GetSchedule,
|
|
18
|
+
* scheduler:ListSchedules, scheduler:UpdateSchedule
|
|
19
|
+
* Also update Lambda function to allow function invocation from scheduler.amazonaws.com.
|
|
19
20
|
*/
|
|
21
|
+
const schedulerGroupName = "default";
|
|
20
22
|
let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
|
|
21
23
|
logger;
|
|
22
24
|
db;
|
|
23
25
|
jobRepo;
|
|
24
26
|
apiLambdaFunctionArn;
|
|
27
|
+
apiLambdaFunctionRoleArn;
|
|
25
28
|
jobNamespace;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
constructor(logger, db, jobRepo, apiLambdaFunctionArn, jobNamespace, eventBusName = "default") {
|
|
29
|
+
scheduler = new SchedulerClient();
|
|
30
|
+
constructor(logger, db, jobRepo, apiLambdaFunctionArn, apiLambdaFunctionRoleArn, jobNamespace) {
|
|
29
31
|
super(logger, db, jobRepo);
|
|
30
32
|
this.logger = logger;
|
|
31
33
|
this.db = db;
|
|
32
34
|
this.jobRepo = jobRepo;
|
|
33
35
|
this.apiLambdaFunctionArn = apiLambdaFunctionArn;
|
|
36
|
+
this.apiLambdaFunctionRoleArn = apiLambdaFunctionRoleArn;
|
|
34
37
|
this.jobNamespace = jobNamespace;
|
|
35
|
-
this.eventBusName = eventBusName;
|
|
36
38
|
}
|
|
37
39
|
async handleCron(jobInfo) {
|
|
38
40
|
this.logger.debug(`Handle cron`, jobInfo);
|
|
39
41
|
await this.executeJob(jobInfo);
|
|
40
42
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
convertCronToSchedulerExpression(cron) {
|
|
44
|
+
// AWS Scheduler uses a different cron format than standard cron
|
|
45
|
+
// Standard cron: minute hour day month day-of-week
|
|
46
|
+
// AWS Scheduler: minute hour day month day-of-week year
|
|
47
|
+
const parts = cron.split(" ");
|
|
48
|
+
if (parts.length === 5) {
|
|
49
|
+
// Add year field if not present
|
|
50
|
+
return `${cron} *`;
|
|
51
|
+
}
|
|
52
|
+
else if (parts.length === 6) {
|
|
53
|
+
return cron;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
throw Errors.SYSTEM_ERROR(`Invalid cron expression: ${cron}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async cleanupJob(job) {
|
|
60
|
+
if (job.at) {
|
|
61
|
+
await this.cancelJob(job.id);
|
|
62
|
+
}
|
|
51
63
|
}
|
|
52
64
|
async getScheduledJobs() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
try {
|
|
66
|
+
const listCommand = new ListSchedulesCommand({
|
|
67
|
+
GroupName: schedulerGroupName,
|
|
68
|
+
NamePrefix: this.jobNamespace,
|
|
69
|
+
});
|
|
70
|
+
const allSchedules = await this.scheduler.send(listCommand);
|
|
71
|
+
const allJobs = await Promise.all((allSchedules.Schedules || []).map(async (schedule) => {
|
|
72
|
+
const scheduleName = schedule.Name;
|
|
73
|
+
try {
|
|
74
|
+
const getCommand = new GetScheduleCommand({
|
|
75
|
+
Name: scheduleName,
|
|
76
|
+
GroupName: schedulerGroupName,
|
|
77
|
+
});
|
|
78
|
+
const scheduleDetails = await this.scheduler.send(getCommand);
|
|
79
|
+
if (!scheduleDetails.Target?.Arn || !scheduleDetails.Target?.Input) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const jobInfo = JSON.parse(scheduleDetails.Target.Input).requestContext.cronScheduler.data;
|
|
83
|
+
return jobInfo;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.logger.warn(`Failed to get schedule details for ${scheduleName}`, error);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
return allJobs.flatMap((job) => (job ? [job] : []));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
this.logger.error("Failed to get scheduled jobs", error);
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
76
96
|
}
|
|
77
97
|
async _scheduleJob(jobInfo) {
|
|
78
98
|
this.logger.debug("Scheduling job: ", jobInfo);
|
|
79
99
|
if (jobInfo.cron || jobInfo.at) {
|
|
80
100
|
const jobId = `${this.jobNamespace}${jobInfo.id}`;
|
|
81
|
-
let
|
|
101
|
+
let scheduleExpression;
|
|
102
|
+
let flexibleTimeWindow;
|
|
82
103
|
if (jobInfo.at) {
|
|
83
|
-
|
|
104
|
+
// For one-time jobs, use at() expression
|
|
105
|
+
const date = new Date(jobInfo.at);
|
|
106
|
+
scheduleExpression = `at(${date.toISOString()})`;
|
|
107
|
+
// Add flexible time window for one-time jobs to handle slight delays
|
|
108
|
+
flexibleTimeWindow = {
|
|
109
|
+
Mode: "OFF",
|
|
110
|
+
};
|
|
84
111
|
}
|
|
85
112
|
else if (jobInfo.cron) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
// For recurring jobs, use cron() expression
|
|
114
|
+
const schedulerCron = this.convertCronToSchedulerExpression(jobInfo.cron);
|
|
115
|
+
scheduleExpression = `cron(${schedulerCron})`;
|
|
116
|
+
// Add flexible time window for recurring jobs
|
|
117
|
+
flexibleTimeWindow = {
|
|
118
|
+
Mode: "FLEXIBLE",
|
|
119
|
+
MaximumWindowInMinutes: 5, // Allow 5-minute window for execution
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${jobInfo.jobName}`);
|
|
91
124
|
}
|
|
92
|
-
this.logger.debug("
|
|
93
|
-
|
|
94
|
-
await this.eventbridge
|
|
95
|
-
.putRule({
|
|
125
|
+
this.logger.debug("Schedule expression", scheduleExpression);
|
|
126
|
+
const scheduleParams = {
|
|
96
127
|
Name: jobId,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
ScheduleExpression:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
Arn: this.apiLambdaFunctionArn,
|
|
110
|
-
Id: jobId,
|
|
111
|
-
//-- this json will be passed to lambda function as event input
|
|
112
|
-
Input: JSON.stringify({
|
|
113
|
-
requestContext: {
|
|
114
|
-
cronScheduler: {
|
|
115
|
-
data: { ...jobInfo, jobId },
|
|
116
|
-
},
|
|
128
|
+
GroupName: schedulerGroupName,
|
|
129
|
+
Description: `${jobInfo.jobName} - ${scheduleExpression}`,
|
|
130
|
+
ScheduleExpression: scheduleExpression,
|
|
131
|
+
FlexibleTimeWindow: flexibleTimeWindow,
|
|
132
|
+
Target: {
|
|
133
|
+
Arn: this.apiLambdaFunctionArn,
|
|
134
|
+
RoleArn: this.apiLambdaFunctionRoleArn,
|
|
135
|
+
Input: JSON.stringify({
|
|
136
|
+
requestContext: {
|
|
137
|
+
cronScheduler: {
|
|
138
|
+
data: { ...jobInfo, jobId },
|
|
117
139
|
},
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
State: ScheduleState.ENABLED,
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
const createCommand = new CreateScheduleCommand(scheduleParams);
|
|
147
|
+
await this.scheduler.send(createCommand);
|
|
148
|
+
this.logger.debug(`Successfully created schedule: ${jobId}`);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
this.logger.error(`Failed to create schedule: ${jobId}`, error);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
123
154
|
}
|
|
124
155
|
else {
|
|
125
156
|
throw Errors.SYSTEM_ERROR(`Job does not have time config: ${jobInfo.jobName}`);
|
|
126
157
|
}
|
|
127
158
|
}
|
|
128
159
|
async cancelJob(jobId) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
.promise();
|
|
160
|
+
const scheduleName = `${this.jobNamespace}${jobId}`;
|
|
161
|
+
try {
|
|
162
|
+
const deleteCommand = new DeleteScheduleCommand({
|
|
163
|
+
Name: scheduleName,
|
|
164
|
+
GroupName: schedulerGroupName,
|
|
165
|
+
});
|
|
166
|
+
await this.scheduler.send(deleteCommand);
|
|
167
|
+
this.logger.debug(`Successfully deleted schedule: ${scheduleName}`);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
this.logger.error(`Failed to delete schedule: ${scheduleName}`, error);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
143
173
|
}
|
|
144
174
|
};
|
|
145
175
|
AwsJobScheduler = __decorate([
|
|
146
176
|
LogContext(),
|
|
147
177
|
__metadata("design:paramtypes", [AbstractLogger,
|
|
148
178
|
AbstractDbAdapter,
|
|
149
|
-
AbstractJobRepository, String, String,
|
|
179
|
+
AbstractJobRepository, String, String, String])
|
|
150
180
|
], AwsJobScheduler);
|
|
151
181
|
export { AwsJobScheduler };
|
|
@@ -50,6 +50,7 @@ export declare class LocalJobScheduler extends AbstractJobScheduler implements I
|
|
|
50
50
|
private sendJob;
|
|
51
51
|
private processMessage;
|
|
52
52
|
private extendMutexKey;
|
|
53
|
+
protected cleanupJob(): Promise<void>;
|
|
53
54
|
init(): Promise<void>;
|
|
54
55
|
exit(): void;
|
|
55
56
|
protected getScheduledJobs(): Promise<AbstractJob[]>;
|
|
@@ -103,6 +103,9 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
103
103
|
await this.redisClient.setex(this.holdMutexKey, this.keyRetentionDurationSeconds, 1);
|
|
104
104
|
this.logger.debug("Scheduler extends mutex key");
|
|
105
105
|
}
|
|
106
|
+
async cleanupJob() {
|
|
107
|
+
//-- do nothing
|
|
108
|
+
}
|
|
106
109
|
async init() {
|
|
107
110
|
this.logger.debug("LocalJobScheduler init");
|
|
108
111
|
//-- subscribe to multi client channel
|
|
@@ -106,6 +106,7 @@ 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);
|
|
109
110
|
await this.jobScheduler?.handleCron(requestOptions.body);
|
|
110
111
|
return toApiGatewayFormat({ code: 200, headers: corsHeaders, cookies: {} });
|
|
111
112
|
}
|
|
@@ -11,6 +11,7 @@ const parseCookie = (cookieArray) => {
|
|
|
11
11
|
return result;
|
|
12
12
|
};
|
|
13
13
|
export const lambdaRequestMapper = (event) => {
|
|
14
|
+
console.log("lambda event", event);
|
|
14
15
|
if (event.requestContext.eventType) {
|
|
15
16
|
//-- socket
|
|
16
17
|
const method = event.requestContext.eventType.toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clairejs/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.27.0",
|
|
4
4
|
"description": "Claire server NodeJs framework written in Typescript.",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"author": "immort",
|
|
16
16
|
"license": "ISC",
|
|
17
17
|
"dependencies": {
|
|
18
|
+
"@aws-sdk/client-scheduler": "^3.848.0",
|
|
18
19
|
"aws-sdk": "^2.841.0",
|
|
19
20
|
"axios": "^1.6.2",
|
|
20
21
|
"cookie-parser": "^1.4.6",
|