Skip to content

Commit

Permalink
Implement support for Cron expressions (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
kibertoad authored Jan 4, 2023
1 parent 115b524 commit 9440f5c
Show file tree
Hide file tree
Showing 8 changed files with 421 additions and 7 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,27 @@ console.log(scheduler.getById('id_2').getStatus()); // returns "stopped" and can

```

## Cron support

You can use CronJob instances for handling Cron-style scheduling:
```ts
const task = new AsyncTask('simple task', () => {
// Execute your asynchronous logic here
})
const job = new CronJob(
{
cronExpression: '*/2 * * * * *',
},
task,
{
preventOverrun: true,
}
)
scheduler.addCronJob(job)
```

Note that you need to install "croner" library for this to work. Run `npm i croner` in order to install this dependency.

## Usage in clustered environments

`toad-scheduler` does not persist its state by design, and has no out-of-the-box concurrency management features. In case it is necessary
Expand Down
9 changes: 9 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@ export { JobStatus } from './lib/common/Job'
export { SimpleIntervalJob } from './lib/engines/simple-interval/SimpleIntervalJob'
export { LongIntervalJob } from './lib/engines/simple-interval/LongIntervalJob'
export type { SimpleIntervalSchedule } from './lib/engines/simple-interval/SimpleIntervalSchedule'
export {
CronJob,
CRON_EVERY_30_MINUTES,
CRON_EVERY_30_SECONDS,
CRON_EVERY_HOUR,
CRON_EVERY_MINUTE,
CRON_EVERY_SECOND,
} from './lib/engines/cron/CronJob'
export type { CronSchedule } from './lib/engines/cron/CronJob'
69 changes: 69 additions & 0 deletions lib/engines/cron/CronJob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Job, JobStatus } from '../../common/Job'
import { Task } from '../../common/Task'
import { AsyncTask } from '../../common/AsyncTask'
import { JobOptions } from '../simple-interval/SimpleIntervalJob'

export const CRON_EVERY_SECOND = '* * * * * *'

export const CRON_EVERY_30_SECONDS = '*/30 * * * * *'

export const CRON_EVERY_MINUTE = '* * * * *'

export const CRON_EVERY_30_MINUTES = '*/30 * * * *'

export const CRON_EVERY_HOUR = '0 * * * *'

export type CronSchedule = {
cronExpression: string
timezone?: string
}

export type Cron = {
running(): boolean
stop(): void
}

export class CronJob extends Job {
private readonly schedule: CronSchedule
private readonly task: Task | AsyncTask
private readonly preventOverrun: boolean

private cronInstance: Cron | undefined

constructor(schedule: CronSchedule, task: Task | AsyncTask, options: JobOptions = {}) {
super(options.id)
this.preventOverrun = options.preventOverrun ?? true
this.schedule = schedule
this.task = task
}

getStatus(): JobStatus {
return this.cronInstance?.running() ? JobStatus.RUNNING : JobStatus.STOPPED
}

start(): void {
// lazy-require croner to avoid mandatory dependency
const croner = require('croner')
if (!croner) {
throw new Error(
'Please install "croner" (run "npm i croner") in case you want to use Cron jobs.'
)
}

this.cronInstance = croner.Cron(
this.schedule.cronExpression,
{
timezone: this.schedule.timezone,
},
() => {
if (!this.task.isExecuting || !this.preventOverrun) {
this.task.execute()
}
}
)
}

stop(): void {
this.cronInstance?.stop()
}
}
22 changes: 22 additions & 0 deletions lib/engines/cron/CronJobEngine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { SchedulerEngine } from '../../common/SchedulerEngine'
import { CronJob } from './CronJob'

export class CronJobEngine extends SchedulerEngine<CronJob> {
private readonly jobs: CronJob[]

constructor() {
super()
this.jobs = []
}

add(job: CronJob): void {
this.jobs.push(job)
job.start()
}

stop(): void {
for (const job of this.jobs) {
job.stop()
}
}
}
29 changes: 22 additions & 7 deletions lib/toadScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { SimpleIntervalEngine } from './engines/simple-interval/SimpleIntervalEn
import { SimpleIntervalJob } from './engines/simple-interval/SimpleIntervalJob'
import { Job } from './common/Job'
import { LongIntervalJob } from './engines/simple-interval/LongIntervalJob'
import { CronJob } from './engines/cron/CronJob'
import { CronJobEngine } from './engines/cron/CronJobEngine'

type EngineRegistry = {
simpleIntervalEngine?: SimpleIntervalEngine
cronJobEngine?: CronJobEngine
}

export class ToadScheduler {
Expand All @@ -21,13 +24,7 @@ export class ToadScheduler {
this.engines.simpleIntervalEngine = new SimpleIntervalEngine()
}

if (job.id) {
if (this.jobRegistry[job.id]) {
throw new Error(`Job with an id ${job.id} is already registered.`)
}
this.jobRegistry[job.id] = job
}

this.registerJob(job)
this.engines.simpleIntervalEngine.add(job)
}

Expand All @@ -39,6 +36,24 @@ export class ToadScheduler {
return this.addIntervalJob(job)
}

private registerJob(job: Job): void {
if (job.id) {
if (this.jobRegistry[job.id]) {
throw new Error(`Job with an id ${job.id} is already registered.`)
}
this.jobRegistry[job.id] = job
}
}

addCronJob(job: CronJob): void {
if (!this.engines.cronJobEngine) {
this.engines.cronJobEngine = new CronJobEngine()
}

this.registerJob(job)
this.engines.cronJobEngine.add(job)
}

stop(): void {
for (const engine of Object.values(this.engines)) {
engine?.stop()
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@types/node": "^18.11.17",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"croner": "^5.3.5",
"eslint": "^8.30.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
Expand Down
Loading

0 comments on commit 9440f5c

Please sign in to comment.