Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.x] Sub-minute Scheduling #47279

Merged
merged 13 commits into from
Jun 30, 2023
Merged

[10.x] Sub-minute Scheduling #47279

merged 13 commits into from
Jun 30, 2023

Conversation

jessarcher
Copy link
Member

@jessarcher jessarcher commented May 30, 2023

This PR introduces the ability to schedule tasks with a sub-minute frequencies.

Example

$schedule->job(new UpdateLeaderboard)->everyFiveSeconds();

The new frequency options are:

  • everySecond
  • everyTwoSeconds
  • everyFiveSeconds
  • everyTenSeconds
  • everyFifteenSeconds
  • everyTwentySeconds
  • everyThirtySeconds

How it works

The schedule:run Artisan command has been modified to continue running until the end of the minute when the current minute has events with sub-minute frequency.

image

Caveats

  1. It's possible for slow-running tasks to prevent subsequent tasks from starting on time. This can be avoided by ensuring slow tasks run in the background or are queued.
  2. The sub-minute frequency must be evenly divisible into 60 seconds because every minute is effectively a clean slate. Each task will be run at the start of the minute regardless of when it was last run in the previous minute. This also means it's possible for a task to repeat faster than the configured time if the task was delayed in the previous minute.
  3. If a task is configured to only run on one server, each occurrence for the minute will run on the same server.
  4. If a condition (e.g. using the when method) prevents a sub-minute task from running at the beginning of a minute, it won't be run at all during that minute. Solved!
  5. If maintenance mode prevents a task from running at any time during the minute, the task will not be run for the remainder of the minute, even if maintenance mode is disabled. This ensures that the task will be run with a fresh application instance after maintenance mode has been re-enabled. Note that it is possible for maintenance mode to be enabled and then disabled while a long-running foreground task is also running, in which case the schedule:run command won't know this occurred and will continue processing tasks. The schedule:interrupt command can be used in these scenarios to ensure no further jobs are processed for the current minute, but will include those configured to run in maintenance mode.

Interrupting the execution (e.g. when deploying new code)

The schedule:run command is typically called by your system's cron daemon. When deploying new code, you may need to interrupt the currently-running command. This can be done with the schedule:interrupt command:

php artisan schedule:interrupt

This works similarly to the queue:restart command, which uses the cache to pass a signal value to the running process that will be checked on the next loop. The schedule will continue when your cron daemon next calls schedule:run.

I went with "interrupt" over "restart" or "stop" because neither felt quite right with the way the command will be stopped and then potentially started again in the next minute.

@jessarcher jessarcher marked this pull request as ready for review May 30, 2023 07:19
@henzeb
Copy link
Contributor

henzeb commented May 30, 2023

This would be an awesome addition.

Copy link

@mehmetik mehmetik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look good to me.

@mehmetik
Copy link

I'm coding like crazy and I see this PR. "Wow!" I think, "Sub-minute task scheduling, just what I need!". I'm impressed with the cleanliness and organization of the code, it's clear it's a great contribution.

However, as a software engineer, a few potential improvements and alternative solution suggestions come to mind. Look, when tasks are repeated very frequently, this system can cause some performance issues. Running a task every second, especially for heavy-duty operations, can require high processing power. So here's my advice to my friends who will use this feature: monitor the load of your tasks and the overall performance of your system carefully. To make your operations faster and more efficient, consider making your operations parallel using Laravel queues. Here's an example:

public function handle() { dispatch(new UpdateCatalog)->onQueue('high'); }
Another potential improvement could be to add task prioritization to the Laravel scheduling system. This way, we can determine the importance level of each task. This allows some tasks to run more or less frequently than others. Here's an example:

$schedule->job(new UpdateCatalog)->everyMinute()->priority(1);
$schedule->job(new CleanupLogs)->everyFiveMinutes()->priority(2);

Keep coding, you're doing great! 🖖

Copy link
Contributor

@milwad-dev milwad-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool

@bert-w
Copy link
Contributor

bert-w commented May 30, 2023

"interrupt" has a bit of a special meaning when referring to timers and scheduling, it means requesting attention from the processor to some other element of your code (most often an interrupt handler). Maybe subschedule:restart is an option (which it essentially is since a (max) 60 second subschedule loop is called).

Nevertheless this probably needs (some cumbersome-to-write) tests where this subscheduling is tested. As you noted there will be task-skipping when a (synchronous) task takes longer than the interval, which could even be beneficial (no overlaps) depending on the command you're running.

Just for reference, I have a fairly simple implementation for a single command I need to run every 5 seconds using a wrapped command that takes care of the loop: https://gist.github.com/bert-w/89a1bd46787c2404764bfd56f8a23fd1

(think twice before you decide you need to run code that often)

@michaeldyrynda
Copy link
Contributor

michaeldyrynda commented May 31, 2023

I have a use-case in mind for this where I was just going to dispatch n delayed queued jobs every minute, but this would be nicer!

@maciek-szn
Copy link

There is a similar package: https://github.com/spatie/laravel-short-schedule

@JayBizzle
Copy link
Contributor

Would this allow sub-second scheduling?

@sharryy
Copy link

sharryy commented Jun 18, 2023

That would be cool to have.

@jessarcher jessarcher force-pushed the sub-minute-scheduling branch from dd4ed4a to 4e4358e Compare June 30, 2023 06:18
jessarcher and others added 10 commits June 30, 2023 16:33
* Pass queue from Mailable to SendQueuedMailable job

Fixes issue where the unserialized job has the wrong queue.

* Pass connection from Mailable to SendQueuedMailable job

* Fix property mismatches in SendQueuedMailable

* order

---------

Co-authored-by: Taylor Otwell <taylor@laravel.com>
@taylorotwell taylorotwell merged commit 7c707a1 into 10.x Jun 30, 2023
@taylorotwell taylorotwell deleted the sub-minute-scheduling branch June 30, 2023 21:19
@decadence
Copy link
Contributor

The sub-minute frequency must be evenly divisible into 60 seconds because every minute is effectively a clean slate

This means everyTwelveSeconds method is missing or repeatEvery method should be public so we can pass seconds dynamically. Now we can't set event for every 12 seconds.

@jessarcher

@bert-w
Copy link
Contributor

bert-w commented Jul 2, 2023

The sub-minute frequency must be evenly divisible into 60 seconds because every minute is effectively a clean slate

This means everyTwelveSeconds method is missing or repeatEvery method should be public so we can pass seconds dynamically. Now we can't set event for every 12 seconds.

@jessarcher

Thats awfully specific. What prohibits you from using every 10 seconds or every 15 seconds? I do agree the repeatEvery should become public though.

@decadence
Copy link
Contributor

Thats awfully specific

I agree but don't see any reason why it's missing

@innocenzi
Copy link
Contributor

Wanted to try this feature locally, and I realized schedule:run should probably be ran every seconds now instead of every minute? Also, schedule:list doesn't seem to take sub-minute scheduling into account.

@dennisprudlo
Copy link
Contributor

I don't think that's necessary. As far as I understand from the PR description the schedule:run command is kept alive for the whole minute and determines whether a sub-minute task needs to be run.

@innocenzi
Copy link
Contributor

Yeah, I think that's fine. You just have to wait a minute before sub-minute tasks actually run, it's ok. But schedule:list is a bit confusing.

@bert-w
Copy link
Contributor

bert-w commented Jul 11, 2023

I don't think that's necessary. As far as I understand from the PR description the schedule:run command is kept alive for the whole minute and determines whether a sub-minute task needs to be run.

Not necessary? If you run php artisan schedule:run more often than once per minute, any task that has ->everyMinute() will run for each and every call to schedule:run (the same for any other scheduled task if it "is due"). In other words, only ever run it once per minute. It remains the same for this subminute functionality (only run it once per minute using the * * * * * CRON schedule).

@jessarcher
Copy link
Member Author

#47720 improves schedule:list for sub-minute tasks.

@Leonard18
Copy link

A better use case would be with queues.

@T-Mohamed-adam
Copy link

That so valuable 😉

protected function repeatEvery($seconds)
{
if (60 % $seconds !== 0) {
throw new InvalidArgumentException("The seconds [$seconds] are not evenly divisible by 60.");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this say "60 is not evenly divisible by the seconds [$seconds]" or some other wording.

The current wording implies that seconds needs to be divisible by 60 but that's not what the code does...?

@decadence
Copy link
Contributor

Only way to set task every 3 seconds. repeatSeconds is public but repeatEvery is protected.

$event = Schedule::command(Indicators::class);

$event->repeatSeconds = 3;
$event->everyMinute();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.