Schedulers
See how to create and configure your CRON job schedulers.
Introductionβ
Athenna's scheduler offers a fresh approach to managing scheduled tasks on your server. The scheduler allows you to fluently and expressively define your scheduler within your Athenna application itself. When using the scheduler, only a single process is needed on your server. Your task schedule can be defined in your application's
Path.routes('cron.ts')./src/routes/cron.ts
Path.cron('schedulers/MyScheduler.ts')./src/cron/schedulers/MyScheduler.ts
Under the hood, Athenna uses the node-cron
library to schedule and run your tasks.
Bootstrapping the CRON applicationβ
The CRON application is started by the Ignite.cron() method
in your
Path.bin('main.ts')./bin/main.ts
import { Ignite } from '@athenna/core'
const ignite = await new Ignite().load(import.meta.url)
await ignite.cron()
When booting, Athenna will use the CronKernel class to:
- Register the execution logger if
cron.logger.enabledistrue. - Register the exception handler of your schedulers.
- Register all the schedulers defined inside the
schedulersarray of your.athennarc.jsonfile. - Import your file.
Path.routes('cron.ts')./src/routes/cron.ts
You can also set the following options when calling Ignite.cron():
await ignite.cron({
routePath: Path.routes('cron.ts'),
kernelPath: '#src/cron/CronKernel',
exceptionHandlerPath: '#src/cron/exceptions/Handler'
})
routePath: The path to your CRON route file. By default it uses thecron.routeproperty of.athennarc.jsonorif it is not set.Path.routes('cron.ts')./src/routes/cron.ts
kernelPath: The path to your CRON kernel. By default it uses thecron.kernelproperty of.athennarc.jsonor@athenna/cron/kernels/CronKernelif it is not set. You can create your own extending theCronKernelclass of@athenna/cron.exceptionHandlerPath: The path to your exception handler. Check the error handling documentation for more details.forceIgniteFire: Force Ignite to fire even if it was already fired by another application in the same process.
The route file and kernel paths can also be set in your .athennarc.json:
{
"cron": {
"route": "#routes/cron",
"kernel": "#src/cron/CronKernel"
}
}
Defining Schedulersβ
Schedulers are typically stored in the src/cron/schedulers
directory; however, you are free to choose your own storage
location as long as your schedulers can be imported and registered.
To create a new scheduler, you may use the make:scheduler
Artisan command. This command will create a new scheduler class
in the src/cron/schedulers directory and register it inside
schedulers array of .athennarc.json file. Don't worry if
this directory does not exist in your applicationβit will be
created the first time you run the make:scheduler Artisan command:
node artisan make:scheduler DeleteRecentUsers
This will create the schedulers file and automatically register it for you:
{
"schedulers": [
"#src/cron/schedulers/DeleteRecentUsers" π
]
}
You can change the directory where make:scheduler creates your
schedulers by setting the destination option of the command in
your .athennarc.json file:
{
"commands": {
"make:scheduler": {
"path": "@athenna/cron/commands/MakeSchedulerCommand",
"destination": "./src/app/schedulers"
}
}
}
Defining schedulers logicβ
In this example, we will schedule a handler method to be called
every day at midnight. Within the method we will execute a
database query to clear a table:
import { Database } from '@athenna/database'
import { Scheduler, type Context } from '@athenna/cron'
@Scheduler({ pattern: '0 0 * * *' })
export class DeleteRecentUsers {
public async handler(ctx: Context) {
await Database.table('recent_users').delete()
}
}
The handler method receives the CRON context as its first
argument. Check the CRON context documentation
to see all the properties available on it. All the options of the
@Scheduler() annotation are documented in the
annotations documentation.
You can use Crontab.guru to help you create your CRON pattern, or simply ask ChatGPT π€©.
Only classes annotated with @Scheduler() are scheduled when the
application boots. If a class registered in the schedulers array
of .athennarc.json doesn't have the annotation, Athenna will only
register it in the service container as App/Cron/Schedulers/YourClassName,
but it will never run by itself.
Defining schedulers in route fileβ
If you prefer, you can use the
Path.routes('cron.ts')./src/routes/cron.ts
import { Cron } from '@athenna/cron'
import { Database } from '@athenna/database'
Cron.schedule()
.name('delete_recent_users')
.pattern('0 0 * * *')
.handler(async (ctx) => {
await Database.table('recent_users').delete()
})
The handler() method must always be the last method called,
because it's the one responsible to register your scheduler with
all the options defined before it. It returns the scheduled task
created by node-cron, which you can use to stop and start your
scheduler later:
const task = Cron.schedule()
.pattern('* * * * *')
.handler(() => console.log('running every minute'))
task.stop()
Always define a name() for your route schedulers. The name is
used to identify your scheduler in logs, in traces
and also to run it using Cron.runByName(). Schedulers without
a name receive a random one generated by node-cron.
Listing schedulers (Coming Soon)β
If you would like to view an overview of your scheduled tasks and the next time they are scheduled to run, you may use the cron:list Artisan command:
node artisan cron:list
Scheduler optionsβ
Timezonesβ
By default your CRON pattern will respect the timezone of the
machine running your application. Use the timezone option to
define the timezone that the pattern needs to respect:
@Scheduler({
pattern: '0 0 * * *',
timezone: 'America/Sao_Paulo' π
})
export class DeleteRecentUsers {}
If using routes you may call the timezone() method:
Cron.schedule()
.name('delete_recent_users')
.pattern('0 0 * * *')
.timezone('America/Sao_Paulo') π
.handler(async (ctx) => {})
Registering disabled schedulersβ
Sometimes you might want to register a scheduler without starting
it right away. To do so, set the scheduled option to false. Your
scheduler will be registered but it will only run after you start it
manually using the Cron facade:
@Scheduler({
pattern: '0 0 * * *',
scheduled: false π
})
export class DeleteRecentUsers {}
If using routes you may call the scheduled() method:
Cron.schedule()
.name('delete_recent_users')
.pattern('0 0 * * *')
.scheduled(false) π
.handler(async (ctx) => {})
Then, start it whenever you want:
import { Cron } from '@athenna/cron'
Cron.getTasks().get('DeleteRecentUsers').start()
Recovering missed executionsβ
If the main thread of your application gets blocked for any reason
(a heavy synchronous operation, for example), your scheduler might
miss some of its executions. By default these executions are just
skipped. Set the recoverMissedExecutions option to true to run
all the executions that were missed during this period:
@Scheduler({
pattern: '* * * * *',
recoverMissedExecutions: true π
})
export class SyncProducts {}
If using routes you may call the recoverMissedExecutions() method:
Cron.schedule()
.name('sync_products')
.pattern('* * * * *')
.recoverMissedExecutions(true) π
.handler(async (ctx) => {})
Running scheduler locallyβ
Using runOnInit optionβ
Sometimes you want to run your schedulers right away when
developing locally, without needing to wait for the CRON
pattern. To do so, set the runOnInit=true option:
import { Database } from '@athenna/database'
import { Scheduler, type Context } from '@athenna/cron'
@Scheduler({
runOnInit: true, π
pattern: '0 0 * * *'
})
export class DeleteRecentUsers {
public async handler(ctx: Context) {
await Database.table('recent_users').delete()
}
}
If using routes you may call the runOnInit() method:
import { Cron } from '@athenna/cron'
import { Database } from '@athenna/database'
Cron.schedule().name('delete_recent_users')
.runOnInit(true) π
.pattern('0 0 * * *')
.handler(async (ctx) => {
await Database.table('recent_users').delete()
})
If this option is set to true, it will automatically run your
scheduler when bootstrapping your Athenna application.
A nice trick is to use an environment variable to define this option, this way you can enable it only in your local environment:
import { Env } from '@athenna/config'
@Scheduler({
pattern: '0 0 * * *',
runOnInit: Env('CRON_RUN_ON_INIT', false) π
})
export class DeleteRecentUsers {}
Using Cron.runByName() methodβ
You can also force a scheduler to run at any moment using
the runByName() method of the Cron facade. The name
of class schedulers is the class name by default, and the
name of route schedulers is the one defined with name():
import { Cron } from '@athenna/cron'
await Cron.runByName('DeleteRecentUsers')
await Cron.runByName('delete_recent_users')
If the scheduler does not exist, the NotFoundTaskNameException
will be thrown.
Using cron:run command (Coming Soon)β
When developing or even in production you might need to
force the scheduler to run. To do so you can use the
node artisan cron:run command:
node artisan cron:run DeleteRecentUsers
You can also use a CRON pattern, this will trigger all the schedulers registered with the same pattern:
node artisan cron:run "0 0 * * *"
Dependency injection in schedulersβ
When using schedulers classes you are able to use the @Inject()
annotation to inject dependencies from your application within your
scheduler class:
import { Inject } from '@athenna/ioc'
import { Scheduler, type Context } from '@athenna/cron'
import { RecentUserService } from '#src/services/RecentUserService'
@Scheduler({ pattern: '0 0 * * *' })
export class DeleteRecentUsers {
@Inject()
public recentUserService: RecentUserService
public async handler(ctx: Context) {
await this.recentUserService.deleteAll()
}
}
Automatic constructor injectionβ
You can also use the automatic constructor injection if
you don't want to use the @Inject() annotation:
import { Scheduler, type Context } from '@athenna/cron'
import type { RecentUserService } from '#src/services/RecentUserService'
@Scheduler({ pattern: '0 0 * * *' })
export class DeleteRecentUsers {
public recentUserService: RecentUserService
public constructor(recentUserService: RecentUserService) {
this.recentUserService = recentUserService
}
public async handler(ctx: Context) {
await this.recentUserService.deleteAll()
}
}
The Cron facadeβ
The Cron facade gives you access to all the scheduled tasks of
your application. Besides schedule() and runByName(), the
following methods are available:
Cron.validate()β
Validate if a CRON pattern is valid:
import { Cron } from '@athenna/cron'
Cron.validate('59 * * * *') // true
Cron.validate('60 * * * *') // false
Cron.getTasks()β
Get a Map with all the scheduled tasks, where the key is
the name of the scheduler:
import { Cron } from '@athenna/cron'
const tasks = Cron.getTasks()
tasks.get('DeleteRecentUsers').stop()
tasks.get('DeleteRecentUsers').start()
Cron.close()β
Stop all the scheduled tasks. The tasks will still be registered, so you can start them again later:
import { Cron } from '@athenna/cron'
Cron.close()
Cron.truncate()β
Remove all the scheduled tasks from the registry:
import { Cron } from '@athenna/cron'
Cron.close().truncate()
You don't need to call close() and truncate() when your
application is shutting down. The CronProvider does that for
you during the graceful shutdown
of your application.
Testing schedulersβ
To create a test for your schedulers, use the --cron flag of
the make:test command:
node artisan make:test DeleteRecentUsersTest --cron
This command will create a test class that extends BaseCronTest.
This class will boot your CRON application before running your
tests and close all the tasks after them. Use the scheduler
property of the test context to run your schedulers by name:
import { Database } from '@athenna/database'
import { Test, type Context } from '@athenna/test'
import { BaseCronTest } from '@athenna/core/testing/BaseCronTest'
export default class DeleteRecentUsersTest extends BaseCronTest {
@Test()
public async shouldBeAbleToDeleteRecentUsers({ assert, scheduler }: Context) {
await scheduler.runByName('DeleteRecentUsers')
assert.lengthOf(await Database.table('recent_users').findMany(), 0)
}
}
To have the scheduler property available, register the
scheduler plugin in your
Path.bin('test.ts')./bin/test.ts
import { Runner } from '@athenna/test'
import { scheduler } from '@athenna/cron/testing/plugins'
await Runner.setTsEnv()
.addAssertPlugin()
.addPlugin(scheduler()) π
.addPath('tests/e2e/**/*.ts')
.addPath('tests/unit/**/*.ts')
.setCliArgs(process.argv.slice(2))
.setGlobalTimeout(5000)
.setForceExit()
.run()
You will probably want to set scheduled: false for your schedulers
when running your tests, this way they will only run when you call
scheduler.runByName(). An environment variable defined in your
.env.test file can help you with that:
import { Env } from '@athenna/config'
@Scheduler({
pattern: '0 0 * * *',
scheduled: Env('CRON_SCHEDULED', true) π
})
export class DeleteRecentUsers {}