Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

Digging deeper

Task scheduling

The scheduler runs your app's recurring tasks: you define them in your code, and one cron entry on your server runs them all.

Introduction#

Apps once needed a cron entry for every task. Oriel's scheduler, as Laravel's, keeps the whole schedule in your code, in routes/console.ts, so it is in your repository with the rest of your app. Your server needs one cron entry, which runs schedule:run every minute (see Running the scheduler).

Defining schedules#

Define your tasks with the Schedule facade, in routes/console.ts. This one empties a table every day at midnight:

routes/console.ts
import { DB, Schedule } from '@orieljs/framework'Schedule.call(async () => {  await DB.table('recent_users').delete()}).daily()

A task is a console command, a closure or a job. node oriel schedule:list lists the tasks you have defined, each with its cron expression.

Scheduling commands#

Schedule.command() runs a console command by its name, with its arguments and options in an array, or written into the name:

Schedule.command('emails:send', ['taylor', '--force']).daily()Schedule.command('emails:send taylor --force').daily()

A command fails when it ends with an exit code other than 0.

Scheduling closures#

Schedule.call() runs a function, which may be async, as in the example above. It fails when it throws, or returns false.

Scheduling queued jobs#

Schedule.job() takes a job, and the queue and the connection to push it onto:

import { Heartbeat } from './app/Jobs/Heartbeat'Schedule.job(new Heartbeat(), 'heartbeats', 'redis').everyFiveMinutes()

The job is made when routes/console.ts loads, which is on every node oriel command, so don't read the database to make it: a job that needs a model finds it in its handle().

A queued job, such as one that extends Queueable, is pushed onto its queue each time the task runs, and a worker runs it. Any other job runs at once, in the scheduler's process.

Schedule frequency options#

A task's frequency is a method on it:

MethodRuns the task
cron('* * * * *')On a cron expression of your own
everySecond(), everyTwoSeconds(), everyFiveSeconds(), everyTenSeconds(), everyFifteenSeconds(), everyTwentySeconds(), everyThirtySeconds()Every second, or every 2, 5, 10, 15, 20 or 30 seconds (see Sub-minute tasks)
everyMinute()Every minute
everyTwoMinutes(), everyThreeMinutes(), everyFourMinutes(), everyFiveMinutes(), everyTenMinutes(), everyFifteenMinutes(), everyThirtyMinutes()Every 2, 3, 4, 5, 10, 15 or 30 minutes
hourly()Every hour, on the hour
hourlyAt(17)Every hour at 17 minutes past (or a list of minutes: hourlyAt([0, 30]))
everyOddHour(minute), everyTwoHours(minute), everyThreeHours(minute), everyFourHours(minute), everySixHours(minute)Every odd hour, or every 2, 3, 4 or 6 hours, at minute past (0 when left out)
daily()Every day at midnight
dailyAt('13:00'), at('13:00')Every day at 13:00
twiceDaily(1, 13)Every day at 1:00 and 13:00
twiceDailyAt(1, 13, 15)Every day at 1:15 and 13:15
weekly()Every Sunday at 00:00
weeklyOn(1, '8:00')Every week on Monday at 8:00 (or a list of days)
monthly()On the first day of every month at 00:00
monthlyOn(4, '15:00')Every month on the 4th at 15:00
twiceMonthly(1, 16, '13:00')Every month on the 1st and the 16th at 13:00
lastDayOfMonth('15:00')On the last day of every month at 15:00
quarterly()On the first day of every quarter at 00:00
quarterlyOn(4, '14:00')Every quarter on the 4th at 14:00
yearly()On the first day of every year at 00:00
yearlyOn(6, 1, '17:00')Every year on June 1st at 17:00

Combine a frequency with the constraints below to narrow it, such as a task that runs every hour on weekdays:

Schedule.command('reports:send').weekdays().hourly()

Day constraints#

weekdays(), weekends(), mondays(), tuesdays(), wednesdays(), thursdays(), fridays(), saturdays() and sundays() limit a task to those days. days() takes the days as numbers, with 0 for Sunday:

Schedule.command('emails:send').hourly().days(0, 3)

Between time constraints#

between() runs a task only between two times of day, and unlessBetween() only outside them. A window may cross midnight:

Schedule.command('emails:send').hourly().between('7:00', '22:00')Schedule.command('emails:send').hourly().unlessBetween('23:00', '4:00')

Truth test constraints#

when() runs a task only when its function returns true, and skip() skips it when its function returns true. The function may be async:

Schedule.command('emails:send').daily().when(() => true)Schedule.command('emails:send').daily().skip(async () => (await DB.table('jobs').count()) > 1000)

Environment constraints#

Laravel's environments() isn't in Oriel. Give when() the check instead:

Schedule.command('emails:send').daily().when(() => app().environment('staging', 'production'))

Timezones#

A task's times are read in your app's timezone, timezone in config/app.ts, which is UTC in a new app. timezone() sets another for one task:

Schedule.command('report:generate').timezone('America/New_York').at('2:00')

Preventing task overlaps#

A task runs again when it is due, even if its last run hasn't finished. withoutOverlapping() skips a run while the last one is still going:

Schedule.command('emails:send').everyMinute().withoutOverlapping()

The task holds a cache lock while it runs. In case a run never ends, the lock expires after 24 hours, or after the number of minutes you give: withoutOverlapping(10). node oriel schedule:clear-cache deletes the locks of the tasks that use it, so a task stuck behind a run that was killed runs again.

A closure has no command to tell it apart from the others, so it needs a name first, or withoutOverlapping() throws "A scheduled event name is required to prevent overlapping. Use the 'name' method before 'withoutOverlapping'.":

Schedule.call(async () => {  await DB.table('recent_users').delete()})  .daily()  .name('prune-recent-users')  .withoutOverlapping()

A job is named after its class, so it needs no name().

Running tasks on one server#

When your app runs the scheduler on several servers, onOneServer() runs a task on only one of them each time it is due: the first server to take its lock runs it. The lock is in your default cache store, so every server must share that store, as the database store does with a shared database, or the redis store with a shared Redis. A closure needs a name() here too.

Schedule.command('report:generate').fridays().at('17:00').onOneServer()

Background tasks#

The tasks due at the same time run one after the other. runInBackground() starts a task and goes on with the next one without waiting for it, so a long task doesn't hold the others back. schedule:run still waits for it before it ends:

Schedule.command('analytics:report').daily().runInBackground()

Running the scheduler#

Add one cron entry to your server, which runs schedule:run every minute:

Terminal
* * * * * cd /path-to-your-project && node oriel schedule:run >> /dev/null 2>&1

schedule:run runs each due task once, prints its name, and goes on to the next one when a task fails, after printing the error. With nothing due, it prints "No scheduled commands are ready to run.".

Sub-minute tasks#

A task that runs every few seconds, such as everyTenSeconds(), is due every minute. Its schedule:run runs it again each time those seconds have passed, until the minute ends, then the next minute's schedule:run takes over:

Schedule.call(async () => {  await DB.table('recent_users').delete()}).everyTenSeconds()

Since that schedule:run keeps going for the whole minute, a deploy would wait for it. schedule:interrupt tells it to stop repeating its sub-minute tasks for the rest of the minute; run it as part of your deploy:

Terminal
node oriel schedule:interrupt

The signal is kept in your default cache store until the minute ends.

Running the scheduler locally#

On your own machine, run schedule:work instead of adding a cron entry. It runs the scheduler every minute in the foreground, until you stop it:

Terminal
node oriel schedule:work

When one minute's run fails, as when the cache can't be read, it reports the error and goes on with the next minute. Deployment shows both ways in production.

Task hooks#

before() runs a function before the task, and after() after it, whether it succeeded or not. onSuccess() runs one when it succeeded, and onFailure() when it failed. Each may be async:

Schedule.command('emails:send')  .daily()  .before(() => {    // The task is about to run...  })  .after(() => {    // The task has run...  })  .onSuccess(() => {    // The task succeeded...  })  .onFailure(() => {    // The task failed...  })

then() is another name for after(). A task skipped by a constraint, an overlap or another server runs none of its hooks.

Privacy policy ·

esc