Merge pull request #59965 from nextcloud/feat/cron-handling

feat(cron): more precise execution report
This commit is contained in:
Benjamin Gaussorgues 2026-05-11 16:26:20 +02:00 committed by GitHub
commit 712ce556b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -19,6 +19,7 @@ use OC\Session\CryptoWrapper;
use OC\Session\Memory;
use OC\User\Session;
use OCP\App\IAppManager;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\IJobList;
use OCP\Files\ISetupManager;
use OCP\IAppConfig;
@ -30,6 +31,8 @@ use OCP\Util;
use Psr\Log\LoggerInterface;
class CronService {
private ?IJob $currentJob = null;
/** * @var ?callable $verboseCallback */
private $verboseCallback = null;
@ -153,6 +156,21 @@ class CronService {
}
}
// Try to log and unlock job in case of failure (eg. Allowed memory size exhausted)
register_shutdown_function(function () {
$error = error_get_last();
if ($error === null) {
return;
}
$message = 'Uncatched error when running job ' . $this->currentJob->getId() . ': ' . $error['message'];
$this->logger->error($message);
$this->verboseOutput($message);
if ($this->currentJob instanceof IJob) {
$this->jobList->unlockJob($this->currentJob);
}
});
// We only ask for jobs for 14 minutes, because after 5 minutes the next
// system cron task should spawn and we want to have at most three
// cron jobs running in parallel.
@ -161,6 +179,7 @@ class CronService {
$executedJobs = [];
while ($job = $this->jobList->getNext($onlyTimeSensitive, $jobClasses)) {
$this->currentJob = $job;
if (isset($executedJobs[$job->getId()])) {
$this->jobList->unlockJob($job);
break;
@ -169,20 +188,19 @@ class CronService {
$jobDetails = get_class($job) . ' (id: ' . $job->getId() . ', arguments: ' . json_encode($job->getArgument()) . ')';
$this->logger->debug('CLI cron call has selected job ' . $jobDetails, ['app' => 'cron']);
$timeBefore = time();
$memoryBefore = memory_get_usage();
$memoryPeakBefore = memory_get_peak_usage();
$this->verboseOutput('Starting job ' . $jobDetails);
$startTime = microtime(true);
$referenceMemory = memory_get_usage();
memory_reset_peak_usage();
$job->start($this->jobList);
$timeAfter = time();
$memoryAfter = memory_get_usage();
$memoryPeakAfter = memory_get_peak_usage();
$memoryIncrease = memory_get_usage() - $referenceMemory;
$timeSpent = microtime(true) - $startTime;
$jobMemoryPeak = memory_get_peak_usage() - $referenceMemory;
$cronInterval = 5 * 60;
$timeSpent = $timeAfter - $timeBefore;
if ($timeSpent > $cronInterval) {
$logLevel = match (true) {
$timeSpent > $cronInterval * 128 => ILogger::FATAL,
@ -198,13 +216,13 @@ class CronService {
);
}
if ($memoryAfter - $memoryBefore > 50_000_000) {
$message = 'Used memory grew by more than 50 MB when executing job ' . $jobDetails . ': ' . Util::humanFileSize($memoryAfter) . ' (before: ' . Util::humanFileSize($memoryBefore) . ')';
if ($memoryIncrease > 50 * 1024 * 1024) {
$message = 'Memory leak detected after executing job ' . $jobDetails . '. Memory usage grew by ' . Util::humanFileSize($memoryIncrease) . '.';
$this->logger->warning($message, ['app' => 'cron']);
$this->verboseOutput($message);
}
if ($memoryPeakAfter > 300_000_000 && $memoryPeakBefore <= 300_000_000) {
$message = 'Cron job used more than 300 MB of ram after executing job ' . $jobDetails . ': ' . Util::humanFileSize($memoryPeakAfter) . ' (before: ' . Util::humanFileSize($memoryPeakBefore) . ')';
if ($jobMemoryPeak > 300 * 1024 * 1024) {
$message = 'Cron job used more than 300 MiB of RAM after executing job ' . $jobDetails . ': ' . Util::humanFileSize($jobMemoryPeak) . ')';
$this->logger->warning($message, ['app' => 'cron']);
$this->verboseOutput($message);
}
@ -219,16 +237,19 @@ class CronService {
$this->verboseOutput($message);
}
$this->verboseOutput('Job ' . $jobDetails . ' done in ' . ($timeAfter - $timeBefore) . ' seconds');
$this->verboseOutput('Job ' . $jobDetails . ' done in ' . number_format($timeSpent, 2) . ' seconds');
$this->jobList->setLastJob($job);
$executedJobs[$job->getId()] = true;
unset($job);
if ($timeAfter > $endTime) {
if (time() > $endTime) {
break;
}
}
// Makes sure last error isn't catched by shutdown function
error_clear_last();
}
private function runWeb(string $appMode): void {