Home > front end >  Scheduling a command for the 2nd Tuesday of every month, at 10am
Scheduling a command for the 2nd Tuesday of every month, at 10am

Time:01-13

I've been tasked with setting up a schedule for a command to run on the 2nd Tuesday of each month, at 10am. I haven't been able to find any real way to test when the command will actually run.

This is what I have at the moment:

$schedule->command('foo')
            ->monthly()
            ->tuesdays()
            ->at('10:00')
            ->when(function () {
                return Carbon::now()->weekOfMonth == 2;
            })
            ->withoutOverlapping()
            ->appendOutputTo(storage_path($logPath));

Am I correct in thinking that the 'when' closure is evaluated only when the command is actually scheduled to run? Do I even need the 'monthly' condition? And most importantly: will this only run on the 2nd Tuesday of each month, at 10am?

Also, is there a way I could test this schedule without running the command (e.g. by passing in a date and getting a true/false if it'll run)?

Thanks

CodePudding user response:

The scheduler helper methods just build a cron expression, so we can look at the expression from your example to see if it does what you expect. Running:

(new Illuminate\Console\Scheduling\Schedule())->command('')->monthly()->tuesdays()->at('10:00')->expression

return "0 10 1 * 2" which means run at 10am on the 1st of every month and and Tuesdays. (You can check that, and other expressions, here).

Your when filter will ensure it does not get run on the 1st of the month, but there is no need for the monthly() call as without it you get the expression "0 10 * * 2" which means at 10am on Tuesdays.

As for testing if it works you can mock the current time for Carbon. The following code will check if the command will run at the specified time:

$schedule = new \Illuminate\Console\Scheduling\Schedule();
$event = $schedule->command('foo')
            ->tuesdays()
            ->at('10:00')
            ->when(function () {
                return Carbon::now()->weekOfMonth == 2;
            })
            ->withoutOverlapping()
            ->appendOutputTo(storage_path($logPath));

Carbon::setTestNow('2023-01-10 10:00:00');

$event->isDue(app()) && $event->filtersPass(app()); // Returns true

Carbon::setTestNow('2023-01-17 10:00:00');

$event->isDue(app()) && $event->filtersPass(app()); // Returns false

If you wanted to run this in a unit test you can get the schedules defined in the kernel with resolve(Illuminate\Console\Scheduling\Schedule::class)->events(); you can then find the event in question and call the isDue and filtersPass method to see if it would run at the specified time. Laravel also offers some helper methods in the base TestCase class for mocking out the time: https://laravel.com/docs/9.x/mocking#interacting-with-time

CodePudding user response:

From laravel documentation:

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 schedule:list Artisan command

php artisan schedule:list

If I do it with the code you provided this is what I get

 ------------------------------------ ------------ ------------- ---------------------------- 
| Command                            | Interval   | Description | Next Due                   |
 ------------------------------------ ------------ ------------- ---------------------------- 
| '/usr/local/bin/php' 'artisan' foo | 0 10 1 * 2 |             | 2023-01-17 10:00:00  00:00 |
 ------------------------------------ ------------ ------------- ---------------------------- 

And if we take the listed interval and put it into a tool like cronhub's crontab expression generator, it will tell you it will run At 10:00 AM, on day 1 of the month, and on Tuesday. This means that it will only run every 1st of the month, but only if it falls on Tuesday. Although in Next due column it shows it will run next Tuesday 2023-01-17 10:00:00 00:00 I think this is an error in Laravel.

So, what you actually want to do is remove the ->monthly() interval so that the command runs every Tuesday, then a callback function you provided with when() is executed and if it returns true it will proceed, meaning your command will only run every second Tuesday in a month.

Or, you could acomplish the same thing by using the cron expressions directly, like this:

$schedule->command('foo')
    ->cron('0 10 * * TUE#2')
    ->withoutOverlapping();
    ->appendOutputTo(storage_path($logPath));

If I run php artisan schedule:list today, it will correctly list the next due date to be February 14th 2023, which is second Tuesday is that month

 ------------------------------------ ---------------- ------------- ---------------------------- 
| Command                            | Interval       | Description | Next Due                   |
 ------------------------------------ ---------------- ------------- ---------------------------- 
| '/usr/local/bin/php' 'artisan' foo | 0 10 * * TUE#2 |             | 2023-02-14 10:00:00  00:00 |
 ------------------------------------ ---------------- ------------- ---------------------------- 
  • Related