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/, 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/. This one empties a table every day at midnight:
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/ 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:
| Method | Runs 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/, 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:
* * * * * cd /path-to-your-project && node oriel schedule:run >> /dev/null 2>&1schedule: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:
node oriel schedule:interruptThe 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:
node oriel schedule:workWhen 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.