From ce879d2dda85bbe01322c40d4cf1ca8987b633cf Mon Sep 17 00:00:00 2001 From: ChengLei Shao Date: Wed, 27 Sep 2023 19:31:14 +0800 Subject: [PATCH] feat(application): cron job in application (#2730) --- packages/core/server/package.json | 3 +- .../core/server/src/__tests__/cron.test.ts | 58 +++++++++++++++++++ packages/core/server/src/application.ts | 12 ++++ .../core/server/src/cron/cron-job-manager.ts | 56 ++++++++++++++++++ packages/core/test/src/index.ts | 4 +- yarn.lock | 17 +++++- 6 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 packages/core/server/src/__tests__/cron.test.ts create mode 100644 packages/core/server/src/cron/cron-job-manager.ts diff --git a/packages/core/server/package.json b/packages/core/server/package.json index ca1f5f791..9500af88c 100644 --- a/packages/core/server/package.json +++ b/packages/core/server/package.json @@ -44,7 +44,8 @@ "semver": "^7.3.7", "serve-handler": "^6.1.5", "ws": "^8.13.0", - "xpipe": "^1.0.5" + "xpipe": "^1.0.5", + "cron": "^2.4.4" }, "devDependencies": { "@types/semver": "^7.3.9", diff --git a/packages/core/server/src/__tests__/cron.test.ts b/packages/core/server/src/__tests__/cron.test.ts new file mode 100644 index 000000000..6b42d6543 --- /dev/null +++ b/packages/core/server/src/__tests__/cron.test.ts @@ -0,0 +1,58 @@ +import { mockServer, MockServer, waitSecond } from '@nocobase/test'; +import { CronJobManager } from '../cron/cron-job-manager'; + +describe('cron service', () => { + let app: MockServer; + beforeEach(async () => { + app = mockServer(); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('should get cron job manager', async () => { + const cron = app.cronJobManager; + expect(cron).toBeInstanceOf(CronJobManager); + }); + + it('should get new cron instance when app reload', async () => { + const cron1 = app.cronJobManager; + expect(cron1).toBeDefined(); + cron1.start(); + expect(cron1.started).toBeTruthy(); + await app.reload(); + expect(cron1.started).toBeFalsy(); + const cron2 = app.cronJobManager; + expect(cron2).toBeDefined(); + expect(cron1).not.toBe(cron2); + }); + + it('should add cron job', async () => { + const cronManager = app.cronJobManager; + const jestFn = jest.fn(); + cronManager.addJob({ + cronTime: '* * * * * *', + onTick: jestFn, + }); + + expect(jestFn).not.toBeCalled(); + + cronManager.start(); + await waitSecond(2000); + expect(jestFn).toBeCalledTimes(2); + }); + + it('should remove cron job', async () => { + const cronManager = app.cronJobManager; + const jestFn = jest.fn(); + const job = cronManager.addJob({ + cronTime: '* * * * * *', + onTick: jestFn, + }); + + expect(cronManager.jobs.size).toBe(1); + cronManager.removeJob(job); + expect(cronManager.jobs.size).toBe(0); + }); +}); diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 42dfa962a..d85d98b00 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -23,6 +23,8 @@ import { ApplicationVersion } from './helpers/application-version'; import { Locale } from './locale'; import { Plugin } from './plugin'; import { InstallOptions, PluginManager } from './plugin-manager'; +import { CronJob } from 'cron'; +import { CronJobManager } from './cron/cron-job-manager'; const packageJson = require('../package.json'); @@ -141,6 +143,12 @@ export class Application exten return this._maintainingMessage; } + protected _cronJobManager: CronJobManager; + + get cronJobManager() { + return this._cronJobManager; + } + protected _db: Database; get db() { @@ -331,6 +339,8 @@ export class Application exten this._loaded = false; + await this.emitAsync('beforeReload', this, options); + await this.load({ ...options, reload: true, @@ -672,6 +682,8 @@ export class Application exten this.plugins = new Map(); this._acl = createACL(); + this._cronJobManager = new CronJobManager(this); + this.use(logger.middleware, { tag: 'logger' }); if (this._db) { diff --git a/packages/core/server/src/cron/cron-job-manager.ts b/packages/core/server/src/cron/cron-job-manager.ts new file mode 100644 index 000000000..06736cd2a --- /dev/null +++ b/packages/core/server/src/cron/cron-job-manager.ts @@ -0,0 +1,56 @@ +import { CronJob, CronJobParameters } from 'cron'; +import Application from '../application'; + +export class CronJobManager { + private _jobs: Set = new Set(); + + private _started = false; + + constructor(private app: Application) { + app.on('beforeStop', async () => { + this.stop(); + }); + + app.on('afterStart', async () => { + this.start(); + }); + + app.on('beforeReload', async () => { + this.stop(); + }); + } + + public get started() { + return this._started; + } + + public get jobs() { + return this._jobs; + } + + public addJob(options: CronJobParameters) { + const cronJob = new CronJob(options); + this._jobs.add(cronJob); + + return cronJob; + } + + public removeJob(job: CronJob) { + job.stop(); + this._jobs.delete(job); + } + + public start() { + this._jobs.forEach((job) => { + job.start(); + }); + this._started = true; + } + + public stop() { + this._jobs.forEach((job) => { + job.stop(); + }); + this._started = false; + } +} diff --git a/packages/core/test/src/index.ts b/packages/core/test/src/index.ts index 54dc2078c..27a2b4779 100644 --- a/packages/core/test/src/index.ts +++ b/packages/core/test/src/index.ts @@ -11,8 +11,8 @@ export function randomStr() { return Math.random().toString(36).substring(2); } -export const waitSecond = async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)); +export const waitSecond = async (timeout = 1000) => { + await new Promise((resolve) => setTimeout(resolve, timeout)); }; export const startServerWithRandomPort = async (startServer) => { diff --git a/yarn.lock b/yarn.lock index 734d0d426..f068285f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5906,6 +5906,11 @@ version "3.3.0" resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.0.tgz#a61043a62c0a72696c73a0a305c544c96501e006" +"@types/luxon@~3.3.0": + version "3.3.2" + resolved "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz#f6e3524c2486b949a4db445e85d93c8e9886dfe2" + integrity sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ== + "@types/markdown-it-highlightjs@3.3.1": version "3.3.1" resolved "https://registry.npmmirror.com/@types/markdown-it-highlightjs/-/markdown-it-highlightjs-3.3.1.tgz#1e051211e7754ba478449fea7faeab3d177ca892" @@ -9433,6 +9438,14 @@ cron@^2.3.1: dependencies: luxon "^3.2.1" +cron@^2.4.4: + version "2.4.4" + resolved "https://registry.npmjs.org/cron/-/cron-2.4.4.tgz#988c1757b3f288d1dfcc360ee6d80087448916dc" + integrity sha512-MHlPImXJj3K7x7lyUHjtKEOl69CSlTOWxS89jiFgNkzXfvhVjhMz/nc7/EIfN9vgooZp8XTtXJ1FREdmbyXOiQ== + dependencies: + "@types/luxon" "~3.3.0" + luxon "~3.3.0" + croner@~4.1.92: version "4.1.97" resolved "https://registry.npmmirror.com/croner/-/croner-4.1.97.tgz#6e373dc7bb3026fab2deb0d82685feef20796766" @@ -15884,7 +15897,7 @@ luxon@^1.28.0: version "1.28.1" resolved "https://registry.npmmirror.com/luxon/-/luxon-1.28.1.tgz#528cdf3624a54506d710290a2341aa8e6e6c61b0" -luxon@^3.2.1: +luxon@^3.2.1, luxon@~3.3.0: version "3.3.0" resolved "https://registry.npmmirror.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48" @@ -19211,7 +19224,7 @@ qrcode.react@^3.1.0: version "3.1.0" resolved "https://registry.npmmirror.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8" -qs@^6.10.1, qs@^6.11.0, qs@^6.4.0, qs@^6.5.2, qs@^6.9.4: +qs@^6.10.1, qs@^6.11.0, qs@^6.11.2, qs@^6.4.0, qs@^6.5.2, qs@^6.9.4: version "6.11.2" resolved "https://registry.npmmirror.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" dependencies: