@clairejs/server 3.22.15 → 3.23.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,5 +1,9 @@
1
1
  ## Change Log
2
2
 
3
+ #### 3.23.0
4
+
5
+ - improve JobScheduler
6
+
3
7
  #### 3.22.15
4
8
 
5
9
  - skip 4xx log in DefaultHttpRequestHandler
@@ -1,18 +1,23 @@
1
- import { type ITransaction } from "@clairejs/orm";
2
- import { type CustomJobInfo } from "./interfaces";
3
- export declare abstract class AbstractJobRepository<T extends CustomJobInfo> {
1
+ import { GetManyOptions, QueryCondition, type ITransaction } from "@clairejs/orm";
2
+ import { type AbstractJob } from "./interfaces";
3
+ export declare abstract class AbstractJobRepository<T extends AbstractJob = AbstractJob> {
4
4
  /**
5
5
  * Return all persisted jobs
6
6
  */
7
- abstract getJobs(): Promise<T[]>;
7
+ abstract getJobs(query?: QueryCondition<T>, options?: GetManyOptions<T, keyof T>): Promise<T[]>;
8
8
  /**
9
9
  * Save the job info and return a unique id
10
- * @param jobInfo The custom job info to save
10
+ * @param jobInfo The job info to save
11
11
  */
12
- abstract saveJob(jobInfo: T, tx?: ITransaction): Promise<string>;
12
+ abstract insertJob(jobInfo: Partial<T>, tx?: ITransaction): Promise<string>;
13
13
  /**
14
14
  * Remove job info by id
15
- * @param id Unique id of job
15
+ * @param jobId Unique id of job
16
16
  */
17
- abstract removeJobById(id: string, tx?: ITransaction): Promise<void>;
17
+ abstract removeJobById(jobId: string, tx?: ITransaction): Promise<void>;
18
+ /**
19
+ * Remove job info by id
20
+ * @param jobId Unique id of job
21
+ */
22
+ abstract updateJobById(jobId: string, update: Partial<Omit<T, "jobId">>, tx?: ITransaction): Promise<void>;
18
23
  }
@@ -1,42 +1,42 @@
1
1
  import { AbstractLogger } from "@clairejs/core";
2
2
  import { ITransaction, ITransactionFactory } from "@clairejs/orm";
3
- import { CustomJobInfo, JobInfo, JobInfoMetadata, ScheduledJob } from "./interfaces";
4
- export type JobHandlerFn = (job: ScheduledJob, tx: ITransaction) => Promise<ScheduledJob | void>;
3
+ import { AbstractJob, JobInfoMetadata, JobSchedulePayload } from "./interfaces";
4
+ import { AbstractJobRepository } from "./AbstractJobRepository";
5
+ export type JobHandlerFn = (job: AbstractJob) => Promise<AbstractJob | void>;
5
6
  interface JobHandlerMetadata extends JobInfoMetadata {
6
7
  handlerFn: JobHandlerFn;
7
8
  }
8
9
  export declare abstract class AbstractJobScheduler {
9
10
  protected readonly logger: AbstractLogger;
10
11
  protected readonly db: ITransactionFactory;
11
- protected abstract isActiveScheduler(): boolean;
12
+ protected readonly jobRepo: AbstractJobRepository;
12
13
  private _jobs;
13
- constructor(logger: AbstractLogger, db: ITransactionFactory);
14
- protected getAvailableJobInfo(): Promise<JobHandlerMetadata[]>;
15
- abstract getAllScheduledJobs(): Promise<ScheduledJob[]>;
16
- /**
17
- * Return unique job id which can then be used to cancel the job
18
- * @param jobInfo the necessary info to launch the job
19
- */
20
- protected abstract scheduleJob(jobInfo: JobInfo): Promise<string>;
21
- protected abstract afterJob(job: ScheduledJob, tx: ITransaction): Promise<void>;
14
+ constructor(logger: AbstractLogger, db: ITransactionFactory, jobRepo: AbstractJobRepository);
15
+ protected getCurrentJobHandlers(): Promise<JobHandlerMetadata[]>;
22
16
  /**
23
17
  * Sync all jobs to running state. This should be called only at init time.
24
18
  */
25
19
  abstract syncJobs(): Promise<void>;
26
20
  /**
27
21
  * Return unique job id which can then be used to cancel the job
28
- * @param jobInfo the necessary info to launch the job
22
+ * @param payload the necessary info to launch the job
29
23
  */
30
- scheduleJobAt(jobInfo: CustomJobInfo): Promise<string>;
24
+ protected abstract _scheduleJob(payload: JobSchedulePayload): Promise<void>;
25
+ scheduleJob({ jobId, ...payload }: JobSchedulePayload): Promise<string>;
31
26
  /**
32
- * Remove the scheduled job and prevent if from running in the future
27
+ * Cancel the scheduled job and prevent if from running in the future
33
28
  * @param id The job id returned from scheduleJobAt function
34
29
  */
35
- abstract removeJob(id: string, tx?: ITransaction): Promise<void>;
30
+ protected abstract cancelJob(jobId: string): Promise<void>;
31
+ /**
32
+ * Remove the scheduled job and prevent if from running in the future
33
+ * @param jobId The job id returned from scheduleJobAt function
34
+ */
35
+ disableJob(jobId: string, tx?: ITransaction): Promise<void>;
36
36
  /**
37
37
  * Execute the scheduled job
38
38
  * @param job The schedled job info to execute
39
39
  */
40
- protected executeJob(job: ScheduledJob): Promise<void>;
40
+ protected executeJob(job: AbstractJob): Promise<void>;
41
41
  }
42
42
  export {};
@@ -1,14 +1,16 @@
1
- import { getServiceProvider } from "@clairejs/core";
1
+ import { diffData, getServiceProvider } from "@clairejs/core";
2
2
  import { AbstractJobController } from "./AbstractJobController";
3
3
  export class AbstractJobScheduler {
4
4
  logger;
5
5
  db;
6
+ jobRepo;
6
7
  _jobs = null;
7
- constructor(logger, db) {
8
+ constructor(logger, db, jobRepo) {
8
9
  this.logger = logger;
9
10
  this.db = db;
11
+ this.jobRepo = jobRepo;
10
12
  }
11
- async getAvailableJobInfo() {
13
+ async getCurrentJobHandlers() {
12
14
  if (this._jobs === null) {
13
15
  //-- read all job info
14
16
  const injector = getServiceProvider().getInjector();
@@ -20,47 +22,95 @@ export class AbstractJobScheduler {
20
22
  }
21
23
  return this._jobs;
22
24
  }
25
+ async scheduleJob({ jobId, ...payload }) {
26
+ let uniqueId = jobId;
27
+ const tx = await this.db.createTransaction();
28
+ if (!uniqueId) {
29
+ uniqueId = await this.jobRepo.insertJob(payload, tx);
30
+ }
31
+ else {
32
+ const [job] = await this.jobRepo.getJobs({ _eq: { jobId: uniqueId } }, { limit: 1 });
33
+ if (!job) {
34
+ await this.jobRepo.insertJob({ ...payload, jobId: uniqueId }, tx);
35
+ }
36
+ else {
37
+ const diff = diffData(job, payload);
38
+ if (diff) {
39
+ await this.jobRepo.updateJobById(uniqueId, payload, tx);
40
+ }
41
+ }
42
+ }
43
+ await this._scheduleJob({ ...payload, jobId: uniqueId });
44
+ await tx.commit();
45
+ return uniqueId;
46
+ }
23
47
  /**
24
- * Return unique job id which can then be used to cancel the job
25
- * @param jobInfo the necessary info to launch the job
48
+ * Remove the scheduled job and prevent if from running in the future
49
+ * @param jobId The job id returned from scheduleJobAt function
26
50
  */
27
- scheduleJobAt(jobInfo) {
28
- return this.scheduleJob(jobInfo);
51
+ async disableJob(jobId, tx) {
52
+ await this.cancelJob(jobId);
53
+ await this.jobRepo.updateJobById(jobId, { disabled: true }, tx);
29
54
  }
30
55
  /**
31
56
  * Execute the scheduled job
32
57
  * @param job The schedled job info to execute
33
58
  */
34
59
  async executeJob(job) {
60
+ if (job.disabled) {
61
+ return;
62
+ }
35
63
  //-- run job
36
- const allJobs = await this.getAvailableJobInfo();
37
- const jobHandler = allJobs.find((j) => j.jobName === job.jobName);
64
+ const allHandlers = await this.getCurrentJobHandlers();
65
+ const jobHandler = allHandlers.find((j) => j.jobName === job.jobName);
38
66
  const tx = await this.db.createTransaction();
39
- try {
40
- if (!jobHandler) {
41
- //-- remove job
42
- this.logger.info(`Remove job with id: ${job.id} as handler is not found`);
43
- await this.removeJob(job.id, tx);
67
+ if (!jobHandler) {
68
+ this.logger.info(`Disable job with id: ${job.jobId} as handler is not found`);
69
+ await this.disableJob(job.jobId, tx);
70
+ }
71
+ else {
72
+ const update = {
73
+ lastRunAt: Date.now(),
74
+ execCount: (job.execCount || 0) + 1,
75
+ };
76
+ try {
77
+ await jobHandler.handlerFn(job);
78
+ //-- job run success, update
79
+ update.lastSuccessAt = Date.now();
80
+ //-- reset retry count if this is cron job
81
+ if (job.cron) {
82
+ if (job.retryCount) {
83
+ update.retryCount = 0;
84
+ }
85
+ }
86
+ else {
87
+ update.disabled = true;
88
+ }
44
89
  }
45
- else {
46
- const newJob = await jobHandler.handlerFn({ ...job }, tx);
47
- if (job.at) {
48
- if (!newJob) {
49
- await this.removeJob(job.id, tx);
50
- this.logger.info(`Remove one-time job ${job.jobName} at timestamp ${job.at}`);
90
+ catch (err) {
91
+ update.failCount = (job.failCount || 0) + 1;
92
+ update.lastError = String(err);
93
+ //-- job run error, check retry
94
+ if (job.retryOnFail) {
95
+ if ((job.retryCount || 0) < (job.maxRetry || 1)) {
96
+ //-- retry by reschedule the job
97
+ update.retryCount = (job.retryCount || 0) + 1;
98
+ const retryDelay = job.retryDelayMs || 0;
99
+ await this.scheduleJob({ ...job, at: Date.now() + (retryDelay < 60000 ? 60000 : retryDelay) });
51
100
  }
52
101
  else {
53
- await this.afterJob(newJob, tx);
102
+ if (job.cron) {
103
+ update.at = undefined;
104
+ }
105
+ else {
106
+ update.disabled = true;
107
+ await this.cancelJob(job.jobId);
108
+ }
54
109
  }
55
110
  }
56
111
  }
57
- this.logger.debug("Job tx commiting");
58
- await tx.commit();
59
- this.logger.debug("Job tx committed");
60
- }
61
- catch (err) {
62
- this.logger.error("Error handling job", err);
63
- await tx.rollback();
112
+ await this.jobRepo.updateJobById(job.jobId, update, tx);
64
113
  }
114
+ await tx.commit();
65
115
  }
66
116
  }
@@ -1,8 +1,9 @@
1
1
  import { AbstractLogger } from "@clairejs/core";
2
- import { AbstractDbAdapter, ITransaction } from "@clairejs/orm";
2
+ import { AbstractDbAdapter } from "@clairejs/orm";
3
3
  import aws from "aws-sdk";
4
+ import { AbstractJob } from "./interfaces";
4
5
  import { AbstractJobScheduler } from "./AbstractJobScheduler";
5
- import { JobInfo, ScheduledJob } from "./interfaces";
6
+ import { AbstractJobRepository } from "./AbstractJobRepository";
6
7
  /**
7
8
  * Following EventBrige permissions is required: listRules, putRule, deleteRule, listTargetsByRule, putTargets, removeTargets.
8
9
  * Also update Lambda function to alow function invocation from events.amazonaws.com.
@@ -10,18 +11,16 @@ import { JobInfo, ScheduledJob } from "./interfaces";
10
11
  export declare class AwsJobScheduler extends AbstractJobScheduler {
11
12
  protected readonly logger: AbstractLogger;
12
13
  protected readonly db: AbstractDbAdapter;
13
- protected readonly uniqueJobIdFactory: () => string;
14
+ protected readonly jobRepo: AbstractJobRepository;
14
15
  protected readonly apiLambdaFunctionArn: string;
15
16
  protected readonly jobNamespace: string;
16
17
  protected readonly eventBusName: string;
17
18
  protected readonly eventbridge: aws.EventBridge;
18
- constructor(logger: AbstractLogger, db: AbstractDbAdapter, uniqueJobIdFactory: () => string, apiLambdaFunctionArn: string, jobNamespace: string, eventBusName?: string);
19
- handleCron(jobInfo: ScheduledJob): Promise<void>;
20
- protected afterJob(_job: ScheduledJob, _tx: ITransaction): Promise<void>;
19
+ constructor(logger: AbstractLogger, db: AbstractDbAdapter, jobRepo: AbstractJobRepository, apiLambdaFunctionArn: string, jobNamespace: string, eventBusName?: string);
20
+ handleCron(jobInfo: AbstractJob): Promise<void>;
21
21
  private generateCronFromTimestamp;
22
- protected isActiveScheduler(): boolean;
23
- getAllScheduledJobs(): Promise<ScheduledJob[]>;
24
- protected scheduleJob(jobInfo: JobInfo): Promise<string>;
22
+ getAllScheduledJobs(): Promise<AbstractJob[]>;
23
+ protected _scheduleJob(jobInfo: AbstractJob): Promise<void>;
25
24
  syncJobs(): Promise<void>;
26
- removeJob(jobId: string): Promise<void>;
25
+ cancelJob(jobId: string): Promise<void>;
27
26
  }
@@ -12,6 +12,7 @@ import { AbstractDbAdapter } from "@clairejs/orm";
12
12
  import assert from "assert";
13
13
  import aws from "aws-sdk";
14
14
  import { AbstractJobScheduler } from "./AbstractJobScheduler";
15
+ import { AbstractJobRepository } from "./AbstractJobRepository";
15
16
  /**
16
17
  * Following EventBrige permissions is required: listRules, putRule, deleteRule, listTargetsByRule, putTargets, removeTargets.
17
18
  * Also update Lambda function to alow function invocation from events.amazonaws.com.
@@ -19,16 +20,16 @@ import { AbstractJobScheduler } from "./AbstractJobScheduler";
19
20
  let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
20
21
  logger;
21
22
  db;
22
- uniqueJobIdFactory;
23
+ jobRepo;
23
24
  apiLambdaFunctionArn;
24
25
  jobNamespace;
25
26
  eventBusName;
26
27
  eventbridge = new aws.EventBridge();
27
- constructor(logger, db, uniqueJobIdFactory, apiLambdaFunctionArn, jobNamespace, eventBusName = "default") {
28
- super(logger, db);
28
+ constructor(logger, db, jobRepo, apiLambdaFunctionArn, jobNamespace, eventBusName = "default") {
29
+ super(logger, db, jobRepo);
29
30
  this.logger = logger;
30
31
  this.db = db;
31
- this.uniqueJobIdFactory = uniqueJobIdFactory;
32
+ this.jobRepo = jobRepo;
32
33
  this.apiLambdaFunctionArn = apiLambdaFunctionArn;
33
34
  this.jobNamespace = jobNamespace;
34
35
  this.eventBusName = eventBusName;
@@ -37,10 +38,6 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
37
38
  this.logger.debug(`Handle cron`, jobInfo);
38
39
  await this.executeJob(jobInfo);
39
40
  }
40
- async afterJob(_job, _tx) {
41
- //-- do nothing
42
- return;
43
- }
44
41
  generateCronFromTimestamp(at) {
45
42
  const date = new Date(at);
46
43
  return [
@@ -52,9 +49,6 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
52
49
  date.getUTCFullYear(),
53
50
  ].join(" ");
54
51
  }
55
- isActiveScheduler() {
56
- return false;
57
- }
58
52
  async getAllScheduledJobs() {
59
53
  const allRules = await this.eventbridge
60
54
  .listRules({
@@ -78,12 +72,12 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
78
72
  return jobInfo;
79
73
  }));
80
74
  //-- concat with interval jobs because we don't register them
81
- return allJobs.filter((job) => !!job);
75
+ return allJobs.flatMap((job) => (job ? [job] : []));
82
76
  }
83
- async scheduleJob(jobInfo) {
77
+ async _scheduleJob(jobInfo) {
84
78
  this.logger.debug("Scheduling job: ", jobInfo);
85
79
  if (jobInfo.cron || jobInfo.at) {
86
- const jobId = `${this.jobNamespace}${this.uniqueJobIdFactory()}`;
80
+ const jobId = `${this.jobNamespace}${jobInfo.jobId}`;
87
81
  let cronExpression;
88
82
  if (jobInfo.at) {
89
83
  cronExpression = this.generateCronFromTimestamp(jobInfo.at);
@@ -118,7 +112,7 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
118
112
  Input: JSON.stringify({
119
113
  requestContext: {
120
114
  cronScheduler: {
121
- data: { ...jobInfo, id: jobId },
115
+ data: { ...jobInfo, jobId },
122
116
  },
123
117
  },
124
118
  }),
@@ -126,7 +120,6 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
126
120
  ],
127
121
  })
128
122
  .promise();
129
- return jobId;
130
123
  }
131
124
  else {
132
125
  throw Errors.SYSTEM_ERROR(`Job does not have time config: ${jobInfo.jobName}`);
@@ -135,14 +128,12 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
135
128
  async syncJobs() {
136
129
  //-- check jobs
137
130
  const scheduledJobs = await this.getAllScheduledJobs();
138
- const allJobs = await this.getAvailableJobInfo();
131
+ const allHandlers = await this.getCurrentJobHandlers();
139
132
  //-- remove job that no more exist
140
- const nomoreExistJobs = scheduledJobs.filter((job) => !allJobs.find((j) => j.jobName === job.jobName));
133
+ const nomoreExistJobs = scheduledJobs.filter((job) => !allHandlers.find((handler) => handler.jobName === job.jobName));
141
134
  for (const job of nomoreExistJobs) {
142
- if (job.id) {
143
- this.logger.info(`Removing stale job: ${job.jobName} of id: ${job.id}`);
144
- await this.removeJob(job.id);
145
- }
135
+ this.logger.info(`Removing stale job: ${job.jobName} of id: ${job.jobId}`);
136
+ await this.disableJob(job.jobId);
146
137
  }
147
138
  if (nomoreExistJobs.length) {
148
139
  this.logger.info(`Cleaned up: ${nomoreExistJobs.length} stale jobs`);
@@ -150,12 +141,12 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
150
141
  //-- remove scheduled cron jobs that diff the cron expression
151
142
  this.logger.debug("Remove scheduled cron jobs");
152
143
  const scheduledCronJobs = scheduledJobs.filter((j) => j.cron);
153
- const unmatchedCronJobs = scheduledCronJobs.filter((j) => j.cron && !allJobs.find((job) => job.jobName === j.jobName && job.cron === j.cron));
144
+ const unmatchedCronJobs = scheduledCronJobs.filter((j) => j.cron && !allHandlers.find((job) => job.jobName === j.jobName && job.cron === j.cron));
154
145
  if (unmatchedCronJobs.length) {
155
- await Promise.all(unmatchedCronJobs.map((j) => this.removeJob(j.id)));
146
+ await Promise.all(unmatchedCronJobs.map((j) => this.disableJob(j.jobId)));
156
147
  }
157
148
  //-- reschedule new cron jobs and those which are not synced
158
- const resyncCronJobs = allJobs.filter((job) => job.cron &&
149
+ const resyncCronJobs = allHandlers.filter((job) => job.cron &&
159
150
  (unmatchedCronJobs.some((j) => j.jobName === job.jobName) ||
160
151
  !scheduledCronJobs.some((j) => j.jobName === job.jobName)));
161
152
  this.logger.debug("Reschedule cron jobs", resyncCronJobs);
@@ -167,7 +158,7 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
167
158
  }
168
159
  //-- keep "at" jobs as is
169
160
  }
170
- async removeJob(jobId) {
161
+ async cancelJob(jobId) {
171
162
  await this.eventbridge
172
163
  .removeTargets({
173
164
  EventBusName: this.eventBusName,
@@ -186,6 +177,7 @@ let AwsJobScheduler = class AwsJobScheduler extends AbstractJobScheduler {
186
177
  AwsJobScheduler = __decorate([
187
178
  LogContext(),
188
179
  __metadata("design:paramtypes", [AbstractLogger,
189
- AbstractDbAdapter, Function, String, String, Object])
180
+ AbstractDbAdapter,
181
+ AbstractJobRepository, String, String, Object])
190
182
  ], AwsJobScheduler);
191
183
  export { AwsJobScheduler };
@@ -1,15 +1,15 @@
1
1
  import { AbstractLogger, IInit } from "@clairejs/core";
2
2
  import Redis from "ioredis";
3
- import { AbstractDbAdapter, ITransaction } from "@clairejs/orm";
3
+ import { AbstractDbAdapter } from "@clairejs/orm";
4
+ import { AbstractJob } from "./interfaces";
4
5
  import { AbstractJobScheduler } from "./AbstractJobScheduler";
5
- import { CustomJobInfo, JobInfo, ScheduledJob } from "./interfaces";
6
6
  import { AbstractJobRepository } from "./AbstractJobRepository";
7
7
  export declare class LocalJobScheduler extends AbstractJobScheduler implements IInit {
8
8
  protected readonly logger: AbstractLogger;
9
9
  protected readonly db: AbstractDbAdapter;
10
10
  protected readonly redisClient: Redis;
11
11
  protected readonly subscribeClient: Redis;
12
- protected readonly jobRepo: AbstractJobRepository<CustomJobInfo>;
12
+ protected readonly jobRepo: AbstractJobRepository;
13
13
  /**
14
14
  * Redis lock key to select active scheduler
15
15
  */
@@ -18,10 +18,6 @@ export declare class LocalJobScheduler extends AbstractJobScheduler implements I
18
18
  * Redis key to hold active scheduler role
19
19
  */
20
20
  protected readonly holdMutexKey: string;
21
- /**
22
- * Redis key to get unique incremental id
23
- */
24
- protected readonly uniqueIdKey: string;
25
21
  /**
26
22
  * The channel for communication between passive and active schedulers
27
23
  */
@@ -33,9 +29,8 @@ export declare class LocalJobScheduler extends AbstractJobScheduler implements I
33
29
  protected intervals: any[];
34
30
  private mutexHoldInterval?;
35
31
  private isActive;
36
- private notifyResolver;
37
32
  private jobHolder;
38
- constructor(logger: AbstractLogger, db: AbstractDbAdapter, redisClient: Redis, subscribeClient: Redis, jobRepo: AbstractJobRepository<CustomJobInfo>,
33
+ constructor(logger: AbstractLogger, db: AbstractDbAdapter, redisClient: Redis, subscribeClient: Redis, jobRepo: AbstractJobRepository,
39
34
  /**
40
35
  * Redis lock key to select active scheduler
41
36
  */
@@ -44,10 +39,6 @@ export declare class LocalJobScheduler extends AbstractJobScheduler implements I
44
39
  * Redis key to hold active scheduler role
45
40
  */
46
41
  holdMutexKey: string,
47
- /**
48
- * Redis key to get unique incremental id
49
- */
50
- uniqueIdKey: string,
51
42
  /**
52
43
  * The channel for communication between passive and active schedulers
53
44
  */
@@ -59,12 +50,9 @@ export declare class LocalJobScheduler extends AbstractJobScheduler implements I
59
50
  private sendJob;
60
51
  private processMessage;
61
52
  private extendMutexKey;
62
- protected afterJob({ at, ...job }: ScheduledJob, tx: ITransaction): Promise<void>;
63
53
  init(): Promise<void>;
64
54
  exit(): void;
65
- protected isActiveScheduler(): boolean;
66
- getAllScheduledJobs(): Promise<ScheduledJob[]>;
67
55
  syncJobs(): Promise<void>;
68
- protected scheduleJob(jobInfo: JobInfo, tx?: ITransaction): Promise<string>;
69
- removeJob(id: string, tx?: ITransaction): Promise<void>;
56
+ protected _scheduleJob(jobInfo: AbstractJob): Promise<void>;
57
+ protected cancelJob(jobId: string): Promise<void>;
70
58
  }
@@ -12,7 +12,6 @@ import Redis from "ioredis";
12
12
  import Redlock from "redlock";
13
13
  import scheduler from "node-schedule";
14
14
  import { AbstractDbAdapter } from "@clairejs/orm";
15
- import assert from "assert";
16
15
  import { AbstractJobScheduler } from "./AbstractJobScheduler";
17
16
  import { AbstractJobRepository } from "./AbstractJobRepository";
18
17
  var CommunicationMessage;
@@ -20,7 +19,6 @@ var CommunicationMessage;
20
19
  CommunicationMessage["SCHEDULE_JOB"] = "SCHEDULE_JOB";
21
20
  CommunicationMessage["REMOVE_JOB"] = "REMOVE_JOB";
22
21
  CommunicationMessage["SYNC_JOB"] = "SYNC_JOB";
23
- CommunicationMessage["NOTIFY"] = "NOTIFY";
24
22
  })(CommunicationMessage || (CommunicationMessage = {}));
25
23
  let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
26
24
  logger;
@@ -30,13 +28,11 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
30
28
  jobRepo;
31
29
  lockMutexKey;
32
30
  holdMutexKey;
33
- uniqueIdKey;
34
31
  multiClientChannel;
35
32
  keyRetentionDurationSeconds;
36
33
  intervals = [];
37
34
  mutexHoldInterval;
38
35
  isActive = false;
39
- notifyResolver = {};
40
36
  jobHolder = {};
41
37
  constructor(logger, db, redisClient, subscribeClient, jobRepo,
42
38
  /**
@@ -47,10 +43,6 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
47
43
  * Redis key to hold active scheduler role
48
44
  */
49
45
  holdMutexKey,
50
- /**
51
- * Redis key to get unique incremental id
52
- */
53
- uniqueIdKey,
54
46
  /**
55
47
  * The channel for communication between passive and active schedulers
56
48
  */
@@ -59,7 +51,7 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
59
51
  * The time to lock active scheduler
60
52
  */
61
53
  keyRetentionDurationSeconds = 30) {
62
- super(logger, db);
54
+ super(logger, db, jobRepo);
63
55
  this.logger = logger;
64
56
  this.db = db;
65
57
  this.redisClient = redisClient;
@@ -67,26 +59,20 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
67
59
  this.jobRepo = jobRepo;
68
60
  this.lockMutexKey = lockMutexKey;
69
61
  this.holdMutexKey = holdMutexKey;
70
- this.uniqueIdKey = uniqueIdKey;
71
62
  this.multiClientChannel = multiClientChannel;
72
63
  this.keyRetentionDurationSeconds = keyRetentionDurationSeconds;
73
64
  }
74
- sendJob(type, messageId, data) {
75
- this.subscribeClient.publish(this.multiClientChannel, JSON.stringify({ type, messageId, data }));
65
+ sendJob(type, data) {
66
+ this.subscribeClient.publish(this.multiClientChannel, JSON.stringify({ type, data }));
76
67
  }
77
- async processMessage(type, messageId, data) {
68
+ async processMessage(type, data) {
78
69
  switch (type) {
79
70
  case CommunicationMessage.SYNC_JOB:
80
- await this.syncJobs();
81
- this.sendJob(CommunicationMessage.NOTIFY, messageId);
82
- break;
83
- case CommunicationMessage.NOTIFY:
84
- const resolver = this.notifyResolver[messageId];
85
- if (!resolver) {
86
- //-- resolver not found, ignore
71
+ if (!this.isActive) {
72
+ //-- not active scheduler, ignore
87
73
  return;
88
74
  }
89
- resolver(data);
75
+ await this.syncJobs();
90
76
  break;
91
77
  case CommunicationMessage.SCHEDULE_JOB:
92
78
  if (!this.isActive) {
@@ -94,8 +80,7 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
94
80
  return;
95
81
  }
96
82
  const jobInfo = data;
97
- const scheduledId = await this.scheduleJobAt(jobInfo);
98
- this.sendJob(CommunicationMessage.NOTIFY, messageId, scheduledId);
83
+ await this.scheduleJob(jobInfo);
99
84
  break;
100
85
  case CommunicationMessage.REMOVE_JOB:
101
86
  if (!this.isActive) {
@@ -103,8 +88,7 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
103
88
  return;
104
89
  }
105
90
  const jobId = data;
106
- await this.removeJob(jobId);
107
- this.sendJob(CommunicationMessage.NOTIFY, messageId);
91
+ await this.disableJob(jobId);
108
92
  break;
109
93
  default:
110
94
  this.logger.error(`Not recognize message type ${type}`);
@@ -119,10 +103,6 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
119
103
  await this.redisClient.setex(this.holdMutexKey, this.keyRetentionDurationSeconds, 1);
120
104
  this.logger.debug("Scheduler extends mutex key");
121
105
  }
122
- async afterJob({ at, ...job }, tx) {
123
- assert.ok(at);
124
- await this.jobRepo.saveJob({ ...job, at }, tx);
125
- }
126
106
  async init() {
127
107
  this.logger.debug("LocalJobScheduler init");
128
108
  //-- subscribe to multi client channel
@@ -131,7 +111,7 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
131
111
  if (channel === this.multiClientChannel) {
132
112
  //-- process message
133
113
  const payload = JSON.parse(message);
134
- this.processMessage(payload.type, payload.messageId, payload.data).catch((err) => this.logger.error(`Fail to process message, ${payload}`, err));
114
+ this.processMessage(payload.type, payload.data).catch((err) => this.logger.error(`Fail to process message, ${payload}`, err));
135
115
  }
136
116
  });
137
117
  await this.subscribeClient.subscribe(this.multiClientChannel);
@@ -162,67 +142,42 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
162
142
  }
163
143
  this.logger.debug("LocalJobScheduler exit");
164
144
  }
165
- isActiveScheduler() {
166
- return this.isActive;
167
- }
168
- async getAllScheduledJobs() {
169
- return Object.values(this.jobHolder)
170
- .filter((j) => !!j?.jobInfo)
171
- .map((info) => info?.jobInfo);
172
- }
173
145
  async syncJobs() {
174
146
  if (this.isActive) {
175
147
  //-- schedule all cron & interval jobs
176
- const allJobs = await this.getAvailableJobInfo();
148
+ const allJobs = await this.getCurrentJobHandlers();
177
149
  for (const job of allJobs) {
178
150
  if (job.cron) {
179
151
  await this.scheduleJob(job);
180
152
  }
181
153
  }
182
- //-- re-schedule jobs that are stored in repo
183
- const allPersistedJobs = await this.jobRepo.getJobs();
154
+ //-- re-schedule "at" jobs that are stored in repo
155
+ const allPersistedJobs = await this.jobRepo.getJobs({ _neq: { disabled: true, at: undefined } });
184
156
  //-- run job anyway, expired job will be removed then
185
157
  for (const job of allPersistedJobs) {
186
158
  await this.scheduleJob(job);
187
159
  }
188
160
  }
189
161
  else {
190
- const uniqueMessageId = await this.redisClient.incr(this.uniqueIdKey);
191
- return new Promise((resolve) => {
192
- this.notifyResolver[uniqueMessageId] = resolve;
193
- this.sendJob(CommunicationMessage.SYNC_JOB, uniqueMessageId);
194
- });
162
+ this.sendJob(CommunicationMessage.SYNC_JOB);
195
163
  }
196
164
  return;
197
165
  }
198
- async scheduleJob(jobInfo, tx) {
166
+ async _scheduleJob(jobInfo) {
199
167
  if (this.isActive) {
200
168
  //-- case each job type
201
169
  if (jobInfo.at) {
202
- //-- create new schedule job
203
- const id = jobInfo.id ||
204
- (await this.jobRepo.saveJob({
205
- jobName: jobInfo.jobName,
206
- params: jobInfo.params,
207
- at: jobInfo.at,
208
- }, tx));
209
170
  //-- use the lib
210
- const scheduledJob = { ...jobInfo, id };
211
171
  const timeout = setTimeout(() => {
212
- this.executeJob(scheduledJob).catch((err) => this.logger.error(`Error execute job ${scheduledJob.jobName} with id: ${scheduledJob.id}`, err));
172
+ this.executeJob(jobInfo).catch((err) => this.logger.error(`Error execute job ${jobInfo.jobName} with id: ${jobInfo.jobId}`, err));
213
173
  }, new Date(jobInfo.at).getTime() - Date.now());
214
- this.jobHolder[id] = { jobCanceler: () => clearTimeout(timeout), jobInfo: { ...jobInfo, id } };
215
- return id;
174
+ this.jobHolder[jobInfo.jobId] = { jobCanceler: () => clearTimeout(timeout), jobInfo };
216
175
  }
217
176
  else if (jobInfo.cron) {
218
- const id = jobInfo.jobName;
219
- //-- set cron and does not need to persist
220
- const scheduledJob = { ...jobInfo, id };
221
177
  const job = scheduler.scheduleJob(jobInfo.cron, () => {
222
- this.executeJob(scheduledJob).catch((err) => this.logger.error(`Error execute job ${scheduledJob.jobName} with id: ${scheduledJob.id}`, err));
178
+ this.executeJob(jobInfo).catch((err) => this.logger.error(`Error execute job ${jobInfo.jobName} with id: ${jobInfo.jobId}`, err));
223
179
  });
224
- this.jobHolder[id] = { jobCanceler: () => job.cancel(), jobInfo: { ...jobInfo, id } };
225
- return id;
180
+ this.jobHolder[jobInfo.jobId] = { jobCanceler: () => job.cancel(), jobInfo };
226
181
  }
227
182
  else {
228
183
  throw Errors.SYSTEM_ERROR(`Job does not have time config: ${jobInfo.jobName}`);
@@ -230,31 +185,21 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
230
185
  }
231
186
  else {
232
187
  //-- get unique message id
233
- const uniqueMessageId = await this.redisClient.incr(this.uniqueIdKey);
234
- return new Promise((resolve) => {
235
- this.notifyResolver[uniqueMessageId] = resolve;
236
- this.sendJob(CommunicationMessage.SCHEDULE_JOB, uniqueMessageId, jobInfo);
237
- });
188
+ this.sendJob(CommunicationMessage.SCHEDULE_JOB, jobInfo);
238
189
  }
239
190
  }
240
- async removeJob(id, tx) {
191
+ async cancelJob(jobId) {
241
192
  if (this.isActive) {
242
193
  //-- remove from holder
243
- const job = this.jobHolder[id];
194
+ const job = this.jobHolder[jobId];
244
195
  if (job) {
245
196
  job.jobCanceler();
246
- this.jobHolder[id] = undefined;
197
+ this.jobHolder[jobId] = undefined;
247
198
  }
248
- //-- remove from persistence
249
- await this.jobRepo.removeJobById(id, tx);
250
199
  }
251
200
  else {
252
201
  //-- get unique message id
253
- const uniqueMessageId = await this.redisClient.incr(this.uniqueIdKey);
254
- return new Promise((resolve) => {
255
- this.notifyResolver[uniqueMessageId] = resolve;
256
- this.sendJob(CommunicationMessage.REMOVE_JOB, uniqueMessageId, id);
257
- });
202
+ this.sendJob(CommunicationMessage.REMOVE_JOB, jobId);
258
203
  }
259
204
  }
260
205
  };
@@ -265,6 +210,6 @@ LocalJobScheduler = __decorate([
265
210
  AbstractDbAdapter,
266
211
  Redis,
267
212
  Redis,
268
- AbstractJobRepository, String, String, String, String, Number])
213
+ AbstractJobRepository, String, String, String, Number])
269
214
  ], LocalJobScheduler);
270
215
  export { LocalJobScheduler };
@@ -1,6 +1,7 @@
1
+ import { JobRetryOptions } from "./interfaces";
1
2
  import { AbstractJobController } from "./AbstractJobController";
2
3
  import { JobHandlerFn } from "./AbstractJobScheduler";
3
4
  type Descriptor<T> = Omit<TypedPropertyDescriptor<T>, "set">;
4
- export declare const CronJob: (jobName: string, cron: string) => <T extends AbstractJobController>(prototype: T, propertyKey: keyof T, _descriptor: Descriptor<JobHandlerFn>) => void;
5
+ export declare const CronJob: (jobName: string, cron: string, retryOptions?: JobRetryOptions) => <T extends AbstractJobController>(prototype: T, propertyKey: keyof T, _descriptor: Descriptor<JobHandlerFn>) => void;
5
6
  export declare const CustomJob: (jobName: string) => <T extends AbstractJobController>(prototype: T, propertyKey: keyof T, _descriptor: Descriptor<JobHandlerFn>) => void;
6
7
  export {};
@@ -1,13 +1,5 @@
1
1
  import { Errors, initObjectMetadata } from "@clairejs/core";
2
- export const CronJob = (
3
- /**
4
- * Unique name of job
5
- */
6
- jobName,
7
- /**
8
- * Cron expression
9
- */
10
- cron) => (prototype, propertyKey, _descriptor) => {
2
+ export const CronJob = (jobName, cron, retryOptions) => (prototype, propertyKey, _descriptor) => {
11
3
  const metadata = initObjectMetadata(prototype);
12
4
  if (!metadata.jobs) {
13
5
  metadata.jobs = [];
@@ -18,6 +10,7 @@ cron) => (prototype, propertyKey, _descriptor) => {
18
10
  }
19
11
  metadata.jobs.push({
20
12
  jobName,
13
+ retryOptions,
21
14
  cron,
22
15
  handlerName: propertyKey,
23
16
  });
@@ -1,12 +1,13 @@
1
1
  import { AbstractModel, Constructor } from "@clairejs/core";
2
- import { type ITransaction, AbstractDbAdapter } from "@clairejs/orm";
3
- import { CustomJobInfo } from "./interfaces";
2
+ import { type ITransaction, AbstractDbAdapter, GetManyOptions, QueryCondition } from "@clairejs/orm";
3
+ import { AbstractJob } from "./interfaces";
4
4
  import { AbstractJobRepository } from "./AbstractJobRepository";
5
- export declare class DefaultJobRepository<T extends CustomJobInfo & AbstractModel> extends AbstractJobRepository<T> {
5
+ export declare class DefaultJobRepository<T extends AbstractJob & AbstractModel> extends AbstractJobRepository<T> {
6
6
  protected readonly model: Constructor<T>;
7
7
  protected db: AbstractDbAdapter;
8
8
  constructor(model: Constructor<T>, db: AbstractDbAdapter);
9
- getJobs(): Promise<T[]>;
10
- saveJob({ id, ...jobInfo }: T, tx?: ITransaction): Promise<string>;
11
- removeJobById(id: string, tx?: ITransaction): Promise<void>;
9
+ getJobs(query?: QueryCondition<T>, options?: GetManyOptions<T, keyof T>): Promise<T[]>;
10
+ insertJob({ jobId, ...jobInfo }: Partial<T>, tx?: ITransaction): Promise<string>;
11
+ removeJobById(jobId: string, tx?: ITransaction): Promise<void>;
12
+ updateJobById(jobId: string, update: Partial<Omit<T, "jobId">>, tx?: ITransaction): Promise<void>;
12
13
  }
@@ -7,23 +7,25 @@ export class DefaultJobRepository extends AbstractJobRepository {
7
7
  this.model = model;
8
8
  this.db = db;
9
9
  }
10
- async getJobs() {
11
- const jobs = await this.db.use(this.model).getRecords();
12
- return jobs;
10
+ async getJobs(query, options) {
11
+ return await this.db.use(this.model).getRecords(query, options);
13
12
  }
14
- async saveJob({ id, ...jobInfo }, tx) {
15
- if (id) {
13
+ async insertJob({ jobId, ...jobInfo }, tx) {
14
+ if (jobId) {
16
15
  //-- update
17
- await this.db.use(this.model, tx).updateById(id, jobInfo);
16
+ await this.db.use(this.model, tx).updateOne({ _eq: { jobId } }, jobInfo);
17
+ return jobId;
18
18
  }
19
19
  else {
20
20
  //-- create new
21
21
  const job = await this.db.use(this.model, tx).createOne(jobInfo);
22
- id = job.id;
22
+ return job.jobId;
23
23
  }
24
- return id;
25
24
  }
26
- async removeJobById(id, tx) {
27
- await this.db.use(this.model, tx).deleteById(id);
25
+ async removeJobById(jobId, tx) {
26
+ await this.db.use(this.model, tx).deleteMany({ _eq: { jobId } });
27
+ }
28
+ async updateJobById(jobId, update, tx) {
29
+ await this.db.use(this.model, tx).updateOne({ _eq: { jobId } }, update);
28
30
  }
29
31
  }
@@ -1,10 +1,19 @@
1
1
  import { ObjectMetadata } from "@clairejs/core";
2
2
  export declare const CRON_REQUEST_METHOD = "cron";
3
+ export interface JobRetryOptions {
4
+ retryOnFail?: boolean;
5
+ maxRetry?: number;
6
+ retryDelayMs?: number;
7
+ }
3
8
  export interface JobInfoMetadata {
4
9
  /**
5
10
  * Unique name of job
6
11
  */
7
12
  jobName: string;
13
+ /**
14
+ * Retry options
15
+ */
16
+ retryOptions?: JobRetryOptions;
8
17
  /**
9
18
  * Run with cron expression, does not support seconds precision
10
19
  */
@@ -21,19 +30,20 @@ export interface JobInfoMetadata {
21
30
  export interface JobControllerMetadata extends ObjectMetadata {
22
31
  jobs?: JobInfoMetadata[];
23
32
  }
24
- export interface CustomJobInfo {
25
- id?: string;
26
- jobName: string;
27
- at: number;
28
- params?: any;
29
- }
30
- export interface JobInfo {
31
- id?: string;
33
+ export interface JobSchedulePayload extends JobRetryOptions {
34
+ jobId?: string;
32
35
  jobName: string;
33
36
  params?: any;
34
37
  at?: number;
35
38
  cron?: string;
36
39
  }
37
- export interface ScheduledJob extends JobInfo {
38
- id: string;
40
+ export interface AbstractJob extends JobSchedulePayload {
41
+ jobId: string;
42
+ disabled?: boolean;
43
+ execCount?: number;
44
+ failCount?: number;
45
+ retryCount?: number;
46
+ lastError?: string;
47
+ lastRunAt?: number;
48
+ lastSuccessAt?: number;
39
49
  }
package/dist/job/job.d.ts CHANGED
@@ -1,7 +1,19 @@
1
1
  import { AbstractModel } from "@clairejs/core";
2
- import { CustomJobInfo } from "./interfaces";
3
- export declare abstract class AbstractJob extends AbstractModel implements CustomJobInfo {
2
+ import { AbstractJob } from "./interfaces";
3
+ export declare abstract class AbstractJobModel extends AbstractModel implements AbstractJob {
4
+ jobId: string;
4
5
  jobName: string;
5
- at: number;
6
+ at?: number;
7
+ cron?: string;
6
8
  params?: any;
9
+ retryOnFail?: boolean;
10
+ maxRetry?: number;
11
+ retryCount?: number;
12
+ retryDelay?: number;
13
+ disabled?: boolean;
14
+ execCount?: number;
15
+ failCount?: number;
16
+ lastError?: string;
17
+ lastRunAt?: number;
18
+ lastSuccessAt?: number;
7
19
  }
package/dist/job/job.js CHANGED
@@ -8,29 +8,93 @@ var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
10
  import { AbstractModel, Column } from "@clairejs/core";
11
- export class AbstractJob extends AbstractModel {
11
+ export class AbstractJobModel extends AbstractModel {
12
+ jobId;
12
13
  jobName;
13
14
  at;
15
+ cron;
14
16
  params;
17
+ retryOnFail;
18
+ maxRetry;
19
+ retryCount;
20
+ retryDelay;
21
+ disabled;
22
+ execCount;
23
+ failCount;
24
+ lastError;
25
+ lastRunAt;
26
+ lastSuccessAt;
15
27
  }
28
+ __decorate([
29
+ Column({
30
+ description: "Unique id of the job",
31
+ isRequired: true,
32
+ }),
33
+ __metadata("design:type", String)
34
+ ], AbstractJobModel.prototype, "jobId", void 0);
16
35
  __decorate([
17
36
  Column({
18
37
  description: "Name of the job",
19
38
  isRequired: true,
20
39
  }),
21
40
  __metadata("design:type", String)
22
- ], AbstractJob.prototype, "jobName", void 0);
41
+ ], AbstractJobModel.prototype, "jobName", void 0);
23
42
  __decorate([
24
43
  Column({
25
44
  description: "Unix timestamp this job will be triggered",
26
- isRequired: true,
27
45
  isTimestamp: true,
28
46
  }),
29
47
  __metadata("design:type", Number)
30
- ], AbstractJob.prototype, "at", void 0);
48
+ ], AbstractJobModel.prototype, "at", void 0);
49
+ __decorate([
50
+ Column({
51
+ description: "The cron expression to repeat",
52
+ }),
53
+ __metadata("design:type", String)
54
+ ], AbstractJobModel.prototype, "cron", void 0);
31
55
  __decorate([
32
56
  Column({
33
57
  description: "The associated params with this job",
34
58
  }),
35
59
  __metadata("design:type", Object)
36
- ], AbstractJob.prototype, "params", void 0);
60
+ ], AbstractJobModel.prototype, "params", void 0);
61
+ __decorate([
62
+ Column({}),
63
+ __metadata("design:type", Boolean)
64
+ ], AbstractJobModel.prototype, "retryOnFail", void 0);
65
+ __decorate([
66
+ Column({}),
67
+ __metadata("design:type", Number)
68
+ ], AbstractJobModel.prototype, "maxRetry", void 0);
69
+ __decorate([
70
+ Column({}),
71
+ __metadata("design:type", Number)
72
+ ], AbstractJobModel.prototype, "retryCount", void 0);
73
+ __decorate([
74
+ Column({}),
75
+ __metadata("design:type", Number)
76
+ ], AbstractJobModel.prototype, "retryDelay", void 0);
77
+ __decorate([
78
+ Column({}),
79
+ __metadata("design:type", Boolean)
80
+ ], AbstractJobModel.prototype, "disabled", void 0);
81
+ __decorate([
82
+ Column({}),
83
+ __metadata("design:type", Number)
84
+ ], AbstractJobModel.prototype, "execCount", void 0);
85
+ __decorate([
86
+ Column({}),
87
+ __metadata("design:type", Number)
88
+ ], AbstractJobModel.prototype, "failCount", void 0);
89
+ __decorate([
90
+ Column({}),
91
+ __metadata("design:type", String)
92
+ ], AbstractJobModel.prototype, "lastError", void 0);
93
+ __decorate([
94
+ Column({}),
95
+ __metadata("design:type", Number)
96
+ ], AbstractJobModel.prototype, "lastRunAt", void 0);
97
+ __decorate([
98
+ Column({}),
99
+ __metadata("design:type", Number)
100
+ ], AbstractJobModel.prototype, "lastSuccessAt", void 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clairejs/server",
3
- "version": "3.22.15",
3
+ "version": "3.23.0",
4
4
  "description": "Claire server NodeJs framework written in Typescript.",
5
5
  "types": "dist/index.d.ts",
6
6
  "main": "dist/index.js",