@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 CHANGED
@@ -1,6 +1,14 @@
1
1
  ## Change Log
2
2
 
3
- #### 3.27.4
3
+ #### 3.28.0
4
+
5
+ - implement new job scheduler
6
+
7
+ #### 3.27.6
8
+
9
+ - fix npm security issues
10
+
11
+ #### 3.27.5
4
12
 
5
13
  - implement job retry logic
6
14
  - use aws scheduler for jobs
@@ -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 abstract retryJob(payload: AbstractJob): Promise<void>;
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 { 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,78 +22,9 @@ 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
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.jobName === job.jobName);
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
- const retryDelay = job.retryDelayMs || 0;
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 getJobName;
18
+ private getJobUniqueName;
19
19
  private getOneTimeExpression;
20
- protected getScheduledJobs(): Promise<AbstractJob[]>;
21
- protected retryJob(payload: AbstractJob): Promise<void>;
22
- protected _scheduleJob(jobInfo: AbstractJob): Promise<void>;
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, ListSchedulesCommand, ScheduleState, ActionAfterCompletion, UpdateScheduleCommand } 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";
@@ -56,143 +55,95 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
56
55
  }
57
56
  return parts.join(" ");
58
57
  }
59
- getJobName(jobId) {
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 getScheduledJobs() {
67
- try {
68
- const listCommand = new ListSchedulesCommand({
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 allSchedules = await this.scheduler.send(listCommand);
73
- const allJobs = await Promise.all((allSchedules.Schedules || []).map(async (schedule) => {
74
- const scheduleName = schedule.Name;
75
- try {
76
- const getCommand = new GetScheduleCommand({
77
- Name: scheduleName,
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
- catch (error) {
95
- this.logger.error("Failed to get scheduled jobs", error);
96
- 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 || 1) - 1 });
97
91
  }
98
92
  }
99
- async retryJob(payload) {
100
- this.logger.debug("Retrying job: ", payload);
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 _scheduleJob(jobInfo) {
122
- this.logger.debug("Scheduling job: ", jobInfo);
123
- if (jobInfo.cron || jobInfo.at) {
124
- const jobId = `${this.jobNamespace}${jobInfo.id}`;
125
- let scheduleExpression;
126
- let flexibleTimeWindow;
127
- if (jobInfo.at) {
128
- scheduleExpression = this.getOneTimeExpression(jobInfo.at);
129
- // Add flexible time window for one-time jobs to handle slight delays
130
- flexibleTimeWindow = {
131
- Mode: "OFF",
132
- };
133
- }
134
- else if (jobInfo.cron) {
135
- // For recurring jobs, use cron() expression
136
- const schedulerCron = this.convertCronToSchedulerExpression(jobInfo.cron);
137
- scheduleExpression = `cron(${schedulerCron})`;
138
- // Add flexible time window for recurring jobs
139
- flexibleTimeWindow = {
140
- Mode: "FLEXIBLE",
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: ${jobInfo.jobName}`);
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
- const scheduleName = `${this.jobNamespace}${jobId}`;
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 getScheduledJobs(): Promise<AbstractJob[]>;
56
- syncJobs(): Promise<void>;
57
- protected retryJob(payload: AbstractJob): Promise<void>;
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.scheduleJob(jobInfo);
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.disableJob(jobId);
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 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();
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.sendJob(CommunicationMessage.SYNC_JOB);
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 retryJob(payload) {
158
- return await this._scheduleJob(payload);
155
+ async cleanupJob(job) {
156
+ this.jobHolder[job.id] = undefined;
159
157
  }
160
- async _scheduleJob(jobInfo) {
158
+ async registerJob(job) {
161
159
  if (this.isActive) {
162
160
  //-- case each job type
163
- if (jobInfo.at) {
161
+ if (job.at) {
164
162
  //-- use the lib
165
163
  const timeout = setTimeout(() => {
166
- this.executeJob(jobInfo).catch((err) => this.logger.error(`Error execute job ${jobInfo.jobName} with id: ${jobInfo.id}`, err));
167
- }, new Date(jobInfo.at).getTime() - Date.now());
168
- this.jobHolder[jobInfo.id] = { jobCanceler: () => clearTimeout(timeout), jobInfo };
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 (jobInfo.cron) {
171
- const job = scheduler.scheduleJob(jobInfo.cron, () => {
172
- this.executeJob(jobInfo).catch((err) => this.logger.error(`Error execute job ${jobInfo.jobName} with id: ${jobInfo.id}`, err));
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[jobInfo.id] = { jobCanceler: () => job.cancel(), jobInfo };
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: ${jobInfo.jobName}`);
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, jobInfo);
180
+ this.sendJob(CommunicationMessage.SCHEDULE_JOB, job);
183
181
  }
184
182
  }
185
183
  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
  };
@@ -7,31 +7,18 @@ export interface JobRetryOptions {
7
7
  }
8
8
  export interface JobInfoMetadata {
9
9
  /**
10
- * Unique name of job
10
+ * Name of the handler function
11
11
  */
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;
12
+ handlerName: string;
25
13
  /**
26
- * Name of the function handle
14
+ * Unique identifier of the job
27
15
  */
28
- handlerName: string;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clairejs/server",
3
- "version": "3.27.4",
3
+ "version": "3.28.0",
4
4
  "description": "Claire server NodeJs framework written in Typescript.",
5
5
  "types": "dist/index.d.ts",
6
6
  "main": "dist/index.js",