- architecture: introduction, Go modules and workspaces, application lifecycle, request lifecycle - plugins: registration, scheduling, extending, testing - verified Examples for backpack services, towel request context, pact schedules and festival events - TestDocsRequiredPages lists the eight new pages
84 lines
4.4 KiB
Markdown
84 lines
4.4 KiB
Markdown
---
|
|
title: Task scheduling
|
|
description: Run a plugin's console commands on a schedule with pact.HasSchedule, and run the scheduler in the worker, as its own process or from system cron.
|
|
section: plugins
|
|
order: 20
|
|
---
|
|
# Task scheduling
|
|
|
|
WinterCMS plugins schedule work in `registerSchedule`. A SummerCMS plugin declares the same thing by implementing `pact.HasSchedule`: it returns a list of its registered console commands, each with the arguments to pass and how often to run it. Only these compiled entries ever run; there is no way to schedule an arbitrary command at runtime.
|
|
|
|
## Defining schedules
|
|
|
|
Each entry is a `pact.ScheduledCommand`: the command name in `namespace:verb` form, its arguments and a `pact.Cadence`. The command must be registered by some plugin through `pact.HasCommands`.
|
|
|
|
```go src=modules/pact/example_test.go#BlogPlugin.Schedule
|
|
// Schedule runs three of the plugin's registered console commands.
|
|
func (p *BlogPlugin) Schedule() []pact.ScheduledCommand {
|
|
return []pact.ScheduledCommand{
|
|
{Command: "blog:prune-drafts", Cadence: pact.Daily()},
|
|
{Command: "blog:send-digest", Cadence: pact.DailyAt(7, 30)},
|
|
{Command: "blog:sync-feed", Args: []string{"--quiet"}, Cadence: pact.Every(15 * time.Minute)},
|
|
}
|
|
}
|
|
```
|
|
|
|
Build a cadence with one of three functions:
|
|
|
|
| Function | Runs | Laravel equivalent |
|
|
|----------|------|--------------------|
|
|
| `pact.Daily` | Every day at 00:00. | `->daily()` |
|
|
| `pact.DailyAt` | Every day at the given hour and minute. | `->dailyAt('07:30')` |
|
|
| `pact.Every` | At every multiple of the interval since midnight, so `pact.Every(15 * time.Minute)` runs at :00, :15, :30 and :45. | `->everyFifteenMinutes()` |
|
|
|
|
Times are wall-clock times in the `app.timezone` location (UTC when it is not set). On a daylight saving day a daily entry keeps its wall-clock time.
|
|
|
|
The scheduler checks every entry when a worker starts, and the start fails with an error naming the plugin ID and the entry index when:
|
|
|
|
- the command name is empty or the cadence is the zero `pact.Cadence`;
|
|
- a daily hour or minute is out of range;
|
|
- an `pact.Every` interval is shorter than one second or does not divide 24 hours evenly.
|
|
|
|
A command that no plugin registers does not fail the start: each run logs a warning and is skipped.
|
|
|
|
You can inspect a cadence with `pact.Cadence.At` (the hour and minute of a daily cadence) and `pact.Cadence.Interval` (24 hours for a daily cadence). This example prints the entries above:
|
|
|
|
```go src=modules/pact/example_test.go#ExampleHasSchedule
|
|
var plugin pact.HasSchedule = &BlogPlugin{}
|
|
for _, entry := range plugin.Schedule() {
|
|
if hour, minute, daily := entry.Cadence.At(); daily {
|
|
fmt.Printf("%s %v: daily at %02d:%02d\n", entry.Command, entry.Args, hour, minute)
|
|
continue
|
|
}
|
|
fmt.Printf("%s %v: every %s\n", entry.Command, entry.Args, entry.Cadence.Interval())
|
|
}
|
|
// Output:
|
|
// blog:prune-drafts []: daily at 00:00
|
|
// blog:send-digest []: daily at 07:30
|
|
// blog:sync-feed [--quiet]: every 15m0s
|
|
```
|
|
|
|
## How schedules run
|
|
|
|
The schedule runs inside the background job worker from [conga](../../modules/conga/README.md). Every worker turns each entry into a periodic job with the ID `<plugin id>[<index>]:<command>`, for example `acme.blog[0]:blog:prune-drafts`. When several instances of the application run, one worker is elected leader and only the leader enqueues due runs, so each period runs once across all instances, even when the leader changes mid-period.
|
|
|
|
Each run is a job on the `scheduled` queue with a single attempt: an interrupted run is not retried, and the next period runs normally. The worker calls the command in-process and logs its output line by line.
|
|
|
|
The worker runs in `serve` by default. When you run workers separately (`queue.work_in_serve` set to `false`), `./bin/acme queue:work` carries the schedule too.
|
|
|
|
## Running the scheduler on its own
|
|
|
|
To run only the scheduler in its own process, use `schedule:run`. It starts a worker on the `scheduled` queue and runs until it receives SIGINT or SIGTERM:
|
|
|
|
```sh
|
|
./bin/acme schedule:run
|
|
```
|
|
|
|
If you prefer system cron, as in Laravel, use `schedule:run --once` every minute. It runs, without the job queue, every entry that is due in the current minute and exits:
|
|
|
|
```sh
|
|
* * * * * cd /srv/acme && ./bin/acme schedule:run --once
|
|
```
|
|
|
|
`schedule:run --once` has no overlap lock: two runs in the same minute run the due entries twice, as Laravel does. From the application directory during development, `summer schedule:run --once` runs the same command through the built binary.
|