To run tasks when they are due, the schedule should be run every minute on your production server(s) indefinitely.
The schedule doesn't have to be run every minute but if it isn't, jobs scheduled in between the frequency you choose will never run. If you are careful when choosing task frequencies, this might not be an issue. If not running every minute, it must be run at predictable times like every hour, exactly on the hour (ie 08:00, not 08:01).
If multiple tasks are due at the same time, they are run synchronously in the order they were defined. If you define tasks in multiple places (Configuration, Builder Service, Kernel, Self-Scheduling Commands) only the order of tasks defined in each place is guaranteed.
Shipped with this bundle is a schedule:run
console command. Running this command determines the due tasks (if any) for
the current time and runs them.
The most common way to run the schedule is a Cron job that runs the
schedule:run
every minute. The following
should be added to your production server's
crontab:
* * * * * cd /path-to-your-project && php bin/console schedule:run >> /dev/null 2>&1
The Symfony Cloud platform has the ability to configure Cron jobs. Add the following configuration to run your schedule every minute:
# .symfony.cloud.yaml
cron:
spec: * * * * *
cmd: bin/console schedule:run
# ...
View the full Cron Jobs Documentation
If you don't have the ability to run Cron jobs on your server there may be other ways to run the schedule.
The schedule can alternatively be run in your code. Behind the scenes, the
schedule:run
command invokes the ScheduleRunner
service which does all the work. The return value of ScheduleRunner::__invoke()
is a
ScheduleRunContext
object.
The following is a list of alternative scheduling options (please add your own solutions via PR):
Perhaps you have a service that can ping an endpoint (/run-schedule
) defined in
your app every minute (AWS Lamda can be configured to do this). This endpoint
can run the schedule:
// src/Controller/RunScheduleController.php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Zenstruck\ScheduleBundle\Schedule\ScheduleRunner;
/**
* @Route("/run-schedule")
*/
class RunScheduleController
{
public function __invoke(ScheduleRunner $scheduleRunner): Response
{
$result = $scheduleRunner();
return new Response('', $result->isSuccessful() ? 200 : 500);
}
}
The schedule:run
command optionally takes
a list of Task ID's. This will force run these
tasks (and no others) even if they are not currently due. This can be useful
for re-running tasks that fail. The task ID is show
in emails/logs and listed in schedule:list --detail
.
It is probable that at some point, a scheduled task will fail. Because the schedule runs in the background, administrators need to be made aware failures.
If multiple tasks are due at the same time, one failure will not prevent the other due tasks from running.
A failing task may or may not be the result of an exception. For instance, a
CommandTask that ran with an exit code of 1
is considered failed but may not be from the result of an exception (the
command could have returned 1
).
The following are different methods of being alerted to failures:
All schedule/task events are logged (if using monolog, on the schedule
channel).
Errors and Exceptions are logged with the ERROR
and CRITICAL
levels respectively.
The log's context contains useful information like duration, memory usage, task output
and the exception (if failed).
The following is an example log file (some context excluded):
[2020-01-20 13:17:13] schedule.INFO: Running 4 due tasks. {"total":22,"due":4}
[2020-01-20 13:17:13] schedule.INFO: Running "CommandTask": my:command
[2020-01-20 13:17:13] schedule.INFO: Successfully ran "CommandTask": my:command
[2020-01-20 13:17:13] schedule.INFO: Running "ProcessTask": fdere -dsdfsd
[2020-01-20 13:17:13] schedule.ERROR: Failure when running "ProcessTask": fdere -dsdfsd
[2020-01-20 13:17:13] schedule.INFO: Running "CallbackTask": some callback
[2020-01-20 13:17:13] schedule.CRITICAL: Exception thrown when running "CallbackTask": some callback
[2020-01-20 13:24:11] schedule.INFO: Running "CommandTask": another:command
[2020-01-20 13:24:11] schedule.INFO: Skipped "CommandTask": another:command {"reason":"the reason for skip..."}
[2020-01-20 13:24:11] schedule.ERROR: 3/4 tasks ran {"total":4,"successful":1,"skipped":1,"failures":2,"duration":"< 1 sec","memory":"10.0 MiB"}
Services like Papertrail can be configured to alert
administrators when a filter
(ie schedule.ERROR OR schedule.CRITICAL
) is matched.
Administrators can be notified via email when tasks fail. This can be configured per task or for the entire schedule.
The schedule:run
command will have an exit code of
1
if one or more tasks fail. The command's output also contains detailed output.
The crontab entry shown above ignores the exit code and
dumps the command's output to /dev/null
but this could be changed to log the
output and/or alert an administrator.
When defining the schedule:run
cron job with Symfony Cloud, you can
prefix the command with croncape
to be alerted via email
when something goes wrong:
# .symfony.cloud.yaml
cron:
spec: * * * * *
cmd: croncape bin/console schedule:run
# ...
You can create a custom schedule extension with a
onScheduleFailure
hook to add your own failure logic.
You can create an event subscriber that listens to the
AfterScheduleEvent
, check if the schedule
failed, and run your own failure logic.
It is important to be assured your schedule is always running. The best method is to use a Cron health monitoring tool like Oh Dear, Cronitor or Healthchecks. These services give you a unique URL endpoint to ping. If the endpoint doesn't receive a ping after a specified amount of time, an administrator is notified.
You can configure your schedule to ping after
running (assumes your endpoint is https://my-health-monitor.com/endpoint
):
# config/packages/zenstruck_schedule.yaml
zenstruck_schedule:
schedule_extensions:
ping_after: https://my-health-monitor.com/endpoint
This will ping the endpoint after the schedule runs (every minute). If this is too frequent, you can configure a PingTask to ping the endpoint at a different frequency:
zenstruck_schedule:
tasks:
- task: ping:https://my-health-monitor/endpoint
description: Health check
frequency: '@hourly'
In this case, a notification from one of these services means your schedule isn't running.
Depending on your deployment strategy, it might be desirable to ensure the schedule does not
run when deploying. One way to do this is to have your deployment script create a .deploying
file somewhere on the webserver at the start of a deploy, then remove when complete. You can
check for this file when building your schedule and skip if it exists:
// src/Kernel.php
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Zenstruck\ScheduleBundle\Schedule;
use Zenstruck\ScheduleBundle\Schedule\ScheduleBuilder;
class Kernel extends BaseKernel implements ScheduleBuilder
{
public function isDeploying(): bool
{
return file_exists('/path/to/file/.deploying');
}
public function buildSchedule(Schedule $schedule): void
{
$schedule->skip('App is deploying...', $this->isDeploying());
}
// ...
}