-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
Description
After performing maintenance on the operating system hosting Pterodactyl, I encountered an issue where all scheduled tasks are executing every minute, regardless of their configured cron expressions
I searched for related issues in this repository and found #3351 and #4536, but neither provided a viable solution. Therefore, I decided to investigate the issue myself
Environment
-
System Timezone: UTC+8, Asia/Shanghai
# timedatectl Local time: Mon 2025-07-07 02:27:34 CST Universal time: Sun 2025-07-06 18:27:34 UTC RTC time: Sun 2025-07-06 18:27:34 Time zone: Asia/Shanghai (CST, +0800) System clock synchronized: yes NTP service: active RTC in local TZ: no
-
Panel Version: 1.11.11.0, installed directly on the system (note: I'm using a fork from https://github.com/pterodactyl-china/panel, but my analysis indicates this issue is unrelated to the fork)
-
Wings Version: 1.11.14.0 (though this issue is unrelated to Wings)
-
Timezone Configuration: All relevant configurations in the system and Pterodactyl panel explicitly set the timezone to
Asia/Shanghai
, including.env
withAPP_TIMEZONE=Asia/Shanghai
Analysis
I first checked the database and found anomalies in the next_run_at
field of scheduled tasks:
MariaDB [panel]> select last_run_at, next_run_at from schedules;
+---------------------+---------------------+
| last_run_at | next_run_at |
+---------------------+---------------------+
| 2025-07-07 02:33:02 | 2025-07-06 18:40:00 | // crontab expr: 0,20,40 * * * *
| 2025-07-07 02:33:03 | 2025-07-06 18:50:00 | // crontab expr: 10,30,50 * * * *
+---------------------+---------------------+
2 rows in set (0.000 sec)
This query was executed at 2025-07-07 02:33 UTC+8. Notably, the next_run_at
values are earlier than last_run_at
. Additionally, next_run_at
is approximately 8 hours behind the current time, suggesting that next_run_at
is being set using the UTC+0 timezone instead of the local timezone (UTC+8, Asia/Shanghai)
Then I looked into the code. Note that I am not familiar with PHP, so please point out any oversights in my analysis
The next_run_at
field is updated after a scheduled task executes, in the following code:
panel/app/Services/Schedules/ProcessScheduleService.php
Lines 37 to 41 in 01fd763
$this->connection->transaction(function () use ($schedule, $task) { | |
$schedule->forceFill([ | |
'is_processing' => true, | |
'next_run_at' => $schedule->getNextRunDate(), | |
])->saveOrFail(); |
The getNextRunDate
function is implemented as follows:
Lines 119 to 126 in 01fd763
public function getNextRunDate(): CarbonImmutable | |
{ | |
$formatted = sprintf('%s %s %s %s %s', $this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_month, $this->cron_day_of_week); | |
return CarbonImmutable::createFromTimestamp( | |
(new CronExpression($formatted))->getNextRunDate()->getTimestamp() | |
); | |
} |
Here lies the bug: the CarbonImmutable::createFromTimestamp
function always returns a date in the UTC+0 timezone, ignoring the system's or configured timezone (Asia/Shanghai):
As a result, getNextRunDate
sets next_run_at
to a date string in UTC+0. When the system or configured timezone is not UTC+0, the scheduling issue arises:
panel/app/Console/Commands/Schedule/ProcessRunnableCommand.php
Lines 23 to 28 in 01fd763
$schedules = Schedule::query() | |
->with('tasks') | |
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status')) | |
->where('is_active', true) | |
->where('is_processing', false) | |
->whereRaw('next_run_at <= NOW()') |
Possible Fix
Modify the Schedule::getNextRunDate
implementation by replacing:
return CarbonImmutable::createFromTimestamp(
(new CronExpression($formatted))->getNextRunDate()->getTimestamp()
);
with:
return CarbonImmutable::instance(
(new CronExpression($formatted))->getNextRunDate()
);
This change ensures that getNextRunDate
inherits the timezone from CronExpression->getNextRunDate()
, avoiding the UTC+0 issue
Additional Information
A similar function in the codebase does not exhibit this issue:
panel/app/Helpers/Utilities.php
Lines 40 to 45 in 01fd763
public static function getScheduleNextRunDate(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon | |
{ | |
return Carbon::instance((new CronExpression( | |
sprintf('%s %s %s %s %s', $minute, $hour, $dayOfMonth, $month, $dayOfWeek) | |
))->getNextRunDate()); | |
} |
This function uses Carbon::instance
, which correctly preserves the timezone of the DateTime
object returned by CronExpression
.
Further confirmation
To confirm, I added debugging code in ProcessScheduleService.php
after line 41 (reference):
$debugInfo = [
'Timestamp: ' . now()->toDateTimeString(),
'Laravel Timezone: ' . config('app.timezone'),
'PHP Default Timezone: ' . date_default_timezone_get(),
'Current Time: ' . now()->toDateTimeString(),
];
$cronExpression = '0,20,40 * * * *';
$first = CarbonImmutable::createFromTimestamp((new CronExpression($cronExpression))->getNextRunDate()->getTimestamp());
$debugInfo[] = 'getNextRunDate: ' . $first->toDateTimeString() . ' (' . $first->getTimezone()->getName() . ')';
$second = Carbon::instance((new CronExpression($cronExpression))->getNextRunDate());
$debugInfo[] = 'getScheduleNextRunDate: ' . $second->toDateTimeString() . ' (' . $second->getTimezone()->getName() . ')';
$third = CarbonImmutable::instance((new CronExpression($cronExpression))->getNextRunDate());
$debugInfo[] = 'getNextRunDate (fixed): ' . $third->toDateTimeString() . ' (' . $third->getTimezone()->getName() . ')';
file_put_contents('/tmp/schedule_timezone_debug.log', implode(PHP_EOL, $debugInfo) . PHP_EOL . str_repeat('-', 50) . PHP_EOL, FILE_APPEND);
After running php artisan down && php artisan queue:restart && php artisan up
, the /tmp/schedule_timezone_debug.log
file showed:
Timestamp: 2025-07-07 02:52:01
Laravel Timezone: Asia/Shanghai
PHP Default Timezone: Asia/Shanghai
Current Time: 2025-07-07 02:52:01
getNextRunDate: 2025-07-06 19:00:00 (+00:00)
getScheduleNextRunDate: 2025-07-07 03:00:00 (Asia/Shanghai)
getNextRunDate (fixed): 2025-07-07 03:00:00 (Asia/Shanghai)
--------------------------------------------------
which further supports my analysis