CRON
See how to schedule tasks inside any type of Athenna application.
Introduction
The CRON application is the best choice when your process exists only to run scheduled tasks. But sometimes you just need a couple of tasks running inside an application that already exists, like a REST API that needs to clean some table every night or refresh some cache every minute.
For these cases you can use the @athenna/cron package directly,
without booting a CRON application. The package is built on top of
node-cron and gives you
the Cron facade to schedule, run and stop your tasks anywhere in
your code.
Installation
First of all you need to install the @athenna/cron package:
npm install @athenna/cron @opentelemetry/api
The @opentelemetry/api package is used by @athenna/cron to
trace the executions
of your tasks, so make sure it is installed in your project even
if you don't plan to use OpenTelemetry.
Then register the CronProvider inside the providers array
of your .athennarc.json file:
{
"providers": [
// Other service providers...
"@athenna/cron/providers/CronProvider"
]
}
The CronProvider registers the Cron facade in the service
container and also stops and removes all your tasks when your
application is shutting down.
Basic usage
Use the Cron.schedule() method to create and register a new task.
The handler() method must always be the last one called, because
it's the one that registers your task with all the options defined
before it:
import { Cron } from '@athenna/cron'
Cron.schedule()
.name('clear_tmp_files')
.pattern('*/5 * * * *')
.handler(async ctx => {
console.log(`Running ${ctx.name} with pattern ${ctx.pattern}`)
})
The ctx argument is the same CRON context
object available in the CRON application.
You can use Crontab.guru to help you create your CRON pattern.
Where to register your tasks
Since your tasks need the Cron facade to be registered in
the service container, a good place to register them is inside
the boot() method of a service provider:
import { Cron } from '@athenna/cron'
import { ServiceProvider } from '@athenna/ioc'
import { Database } from '@athenna/database'
export default class ScheduleProvider extends ServiceProvider {
public get environment() {
return ['http'] 👈
}
public async boot() {
Cron.schedule()
.name('delete_recent_users')
.pattern('0 0 * * *')
.handler(async () => {
await Database.table('recent_users').delete()
})
}
}
Your tasks will run in every process that registers them. If
you run 3 replicas of your REST API, your tasks will run 3 times.
Use the environment getter of your provider to choose the
applications that will register them, or move your tasks to a
dedicated CRON application.
Task options
The following methods can be called before handler() to
configure your task:
name()
Define the name of your task. The name is used to identify your
task in the Cron.getTasks() map and to run it with
Cron.runByName(). When not defined, a random UUID will be used:
Cron.schedule()
.name('my_task')
.pattern('* * * * *')
.handler(() => {})
pattern()
Define the CRON expression that will determine when your task will run:
Cron.schedule()
.pattern('0 0 * * *')
.handler(() => {})
timezone()
Define the timezone that the CRON expression needs to respect:
Cron.schedule()
.pattern('0 0 * * *')
.timezone('America/Sao_Paulo')
.handler(() => {})
runOnInit()
Run your task right after registering it, no matter the CRON expression:
Cron.schedule()
.pattern('0 0 * * *')
.runOnInit(true)
.handler(() => {})
scheduled()
Define if your task should start right after registering it.
When set to false, your task will be registered but it will
only run after you start it:
const task = Cron.schedule()
.pattern('0 0 * * *')
.scheduled(false)
.handler(() => {})
task.start()
recoverMissedExecutions()
Run the executions that were missed while the main thread was blocked instead of skipping them:
Cron.schedule()
.pattern('* * * * *')
.recoverMissedExecutions(true)
.handler(() => {})
Managing tasks
Starting and stopping tasks
The handler() method returns the task created by node-cron.
You can use it to stop and start your task whenever you want:
const task = Cron.schedule()
.pattern('* * * * *')
.handler(() => {})
task.stop()
task.start()
You can also retrieve any named task using the Cron.getTasks()
method, which returns a Map where the key is the task name:
import { Cron } from '@athenna/cron'
const task = Cron.getTasks().get('my_task')
task.stop()
Running tasks manually
Use the Cron.runByName() method to force a task to run at any
moment, no matter its CRON expression:
import { Cron } from '@athenna/cron'
await Cron.runByName('my_task')
If the task does not exist, the NotFoundTaskNameException
will be thrown.
Validating CRON patterns
Use the Cron.validate() method to verify if a CRON pattern is
valid. This is very useful when the pattern comes from your users
or from your database:
import { Cron } from '@athenna/cron'
Cron.validate('59 * * * *') // true
Cron.validate('60 * * * *') // false
Closing and removing tasks
Use the Cron.close() method to stop all your tasks and the
Cron.truncate() method to remove all of them from the registry:
import { Cron } from '@athenna/cron'
Cron.close().truncate()
The CronProvider already calls both methods when your
application is shutting down, so you only need to call them
if you want to stop your tasks before that.
Error handling
When using the package without the CRON application, no
exception handler is registered for your tasks by default.
To handle the errors of your tasks, set an exception handler
using the Cron.setErrorHandler() method before registering
them. You can use the same CronExceptionHandler used by the
CRON application:
import { Cron, CronExceptionHandler } from '@athenna/cron'
Cron.setErrorHandler(new CronExceptionHandler())
The CronExceptionHandler will log your errors using the
exception channel and will respect the logger options of
your
Path.config('cron.ts')./src/config/cron.ts
You can also set any object with a handle() method:
import { Log } from '@athenna/logger'
import { Cron } from '@athenna/cron'
Cron.setErrorHandler({
handle: async ({ error }) => {
Log.error(error)
}
})
Logging executions
To log the CRON context
every time one of your tasks runs, call the Cron.setLogger()
method. Each execution will be logged using the cronjob channel
of your
Path.config('logging.ts')./src/config/logging.ts
import { Cron } from '@athenna/cron'
Cron.setLogger(true)
Using scheduler classes
If you prefer to organize your tasks as scheduler classes,
you can use the CronKernel to register all the schedulers
defined inside the schedulers array of your .athennarc.json
file, exactly like the CRON application does:
import { CronKernel } from '@athenna/cron'
import { ServiceProvider } from '@athenna/ioc'
export default class ScheduleProvider extends ServiceProvider {
public async boot() {
const kernel = new CronKernel()
await kernel.registerLogger()
await kernel.registerExceptionHandler()
await kernel.registerSchedulers()
}
}
The registerLogger() and registerExceptionHandler() methods
will enable the execution logger (if cron.logger.enabled is true)
and the CronExceptionHandler for your schedulers.
To be able to create schedulers with the make:scheduler command,
register it inside the commands property of your .athennarc.json
file:
{
"commands": {
"make:scheduler": "@athenna/cron/commands/MakeSchedulerCommand"
}
}
Testing
In your tests, you can use the Cron facade to run your tasks
directly, without waiting for their CRON expression. Just make
sure your application was booted before, this way the provider
that registers your tasks will be executed. If your tasks live
inside a REST API, for example, extend the BaseHttpTest class:
import { Cron } from '@athenna/cron'
import { Test, type Context } from '@athenna/test'
import { BaseHttpTest } from '@athenna/core/testing/BaseHttpTest'
export default class ClearTmpFilesTest extends BaseHttpTest {
@Test()
public async shouldBeAbleToClearTmpFiles({ assert }: Context) {
await Cron.runByName('clear_tmp_files')
// assert...
}
}
Consider setting scheduled(false) in your test environment,
this way your tasks will only run when you call Cron.runByName().