@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 CHANGED
@@ -1,5 +1,9 @@
1
1
  ## Change Log
2
2
 
3
+ #### 3.28.1
4
+
5
+ - implement new job scheduler
6
+
3
7
  #### 3.27.6
4
8
 
5
9
  - fix npm security issues
@@ -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
- protected abstract getScheduledJobs(): Promise<AbstractJob[]>;
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 _scheduleJob(payload: JobSchedulePayload): Promise<void>;
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(payload: AbstractJob): Promise<void>;
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(job: AbstractJob): Promise<void>;
38
+ protected executeJob(jobId: string): Promise<void>;
43
39
  }
44
40
  export {};
@@ -1,4 +1,4 @@
1
- import { diffData, getServiceProvider } from "@clairejs/core";
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
- * Sync all jobs to running state. This should be called only at init time.
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(job) {
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.jobName === job.jobName);
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
- if (job.retryOnFail) {
151
- if ((job.retryCount || 0) < (job.maxRetry || 1)) {
152
- //-- retry by reschedule the job
153
- update.retryCount = (job.retryCount || 0) + 1;
154
- const retryDelay = job.retryDelayMs || 0;
155
- await this.retryJob({ ...job, ...update, at: Date.now() + (retryDelay < 60000 ? 60000 : retryDelay) });
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.cron) {
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
- protected getScheduledJobs(): Promise<AbstractJob[]>;
20
- protected _scheduleJob(jobInfo: AbstractJob): Promise<void>;
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, ListSchedulesCommand, ScheduleState, ActionAfterCompletion } from "@aws-sdk/client-scheduler";
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 getScheduledJobs() {
64
- try {
65
- const listCommand = new ListSchedulesCommand({
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 allSchedules = await this.scheduler.send(listCommand);
70
- const allJobs = await Promise.all((allSchedules.Schedules || []).map(async (schedule) => {
71
- const scheduleName = schedule.Name;
72
- try {
73
- const getCommand = new GetScheduleCommand({
74
- Name: scheduleName,
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
- catch (error) {
92
- this.logger.error("Failed to get scheduled jobs", error);
93
- return [];
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 _scheduleJob(jobInfo) {
97
- this.logger.debug("Scheduling job: ", jobInfo);
98
- if (jobInfo.cron || jobInfo.at) {
99
- const jobId = `${this.jobNamespace}${jobInfo.id}`;
100
- let scheduleExpression;
101
- let flexibleTimeWindow;
102
- if (jobInfo.at) {
103
- scheduleExpression = this.getOneTimeExpression(jobInfo.at);
104
- // Add flexible time window for one-time jobs to handle slight delays
105
- flexibleTimeWindow = {
106
- Mode: "OFF",
107
- };
108
- }
109
- else if (jobInfo.cron) {
110
- // For recurring jobs, use cron() expression
111
- const schedulerCron = this.convertCronToSchedulerExpression(jobInfo.cron);
112
- scheduleExpression = `cron(${schedulerCron})`;
113
- // Add flexible time window for recurring jobs
114
- flexibleTimeWindow = {
115
- Mode: "FLEXIBLE",
116
- MaximumWindowInMinutes: 5, // Allow 5-minute window for execution
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: ${jobInfo.jobName}`);
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
- const scheduleName = `${this.jobNamespace}${jobId}`;
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 getScheduledJobs(): Promise<AbstractJob[]>;
56
- syncJobs(): Promise<void>;
57
- protected _scheduleJob(jobInfo: AbstractJob): Promise<void>;
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["SYNC_JOB"] = "SYNC_JOB";
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.SYNC_JOB:
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._scheduleJob(jobInfo);
76
+ await this.registerJob(jobInfo);
84
77
  break;
85
- case CommunicationMessage.REMOVE_JOB:
86
- if (!this.isActive) {
87
- //-- not active scheduler, ignore
88
- return;
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.disableJob(jobId);
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 getScheduledJobs() {
146
- return Object.values(this.jobHolder).flatMap((job) => (job?.jobInfo ? [job.jobInfo] : []));
147
- }
148
- async syncJobs() {
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.sendJob(CommunicationMessage.SYNC_JOB);
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 _scheduleJob(jobInfo) {
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 (jobInfo.at) {
166
+ if (job.at) {
161
167
  //-- use the lib
162
168
  const timeout = setTimeout(() => {
163
- this.executeJob(jobInfo).catch((err) => this.logger.error(`Error execute job ${jobInfo.jobName} with id: ${jobInfo.id}`, err));
164
- }, new Date(jobInfo.at).getTime() - Date.now());
165
- this.jobHolder[jobInfo.id] = { jobCanceler: () => clearTimeout(timeout), jobInfo };
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 (jobInfo.cron) {
168
- const job = scheduler.scheduleJob(jobInfo.cron, () => {
169
- this.executeJob(jobInfo).catch((err) => this.logger.error(`Error execute job ${jobInfo.jobName} with id: ${jobInfo.id}`, err));
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[jobInfo.id] = { jobCanceler: () => job.cancel(), jobInfo };
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: ${jobInfo.jobName}`);
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, jobInfo);
185
+ this.sendJob(CommunicationMessage.SCHEDULE_JOB, job);
180
186
  }
181
187
  }
182
188
  async cancelJob(jobId) {
@@ -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 CronJob: (jobName: string, cron: string, retryOptions?: JobRetryOptions) => <T extends AbstractJobController>(prototype: T, propertyKey: keyof T, _descriptor: Descriptor<JobHandlerFn>) => void;
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 {};
@@ -1,31 +1,11 @@
1
- import { Errors, initObjectMetadata } from "@clairejs/core";
2
- export const CronJob = (jobName, cron, retryOptions) => (prototype, propertyKey, _descriptor) => {
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
- jobName,
8
+ jobIdentifier,
29
9
  handlerName: propertyKey,
30
10
  });
31
11
  };
@@ -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
- * Unique name of job
9
+ * Name of the handler function
11
10
  */
12
- jobName: string;
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
- * Name of the function handle
13
+ * Unique identifier of the job
27
14
  */
28
- handlerName: string;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clairejs/server",
3
- "version": "3.27.6",
3
+ "version": "3.28.1",
4
4
  "description": "Claire server NodeJs framework written in Typescript.",
5
5
  "types": "dist/index.d.ts",
6
6
  "main": "dist/index.js",