Files

226 lines
9.7 KiB
PHP

<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Scheduler;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
use TYPO3\CMS\Scheduler\Event\AfterTaskExecutionEvent;
use TYPO3\CMS\Scheduler\Exception\InvalidTaskException;
use TYPO3\CMS\Scheduler\Task\AbstractTask;
use TYPO3\CMS\Scheduler\Task\TaskSerializer;
/**
* TYPO3 Scheduler. This class handles scheduling and execution of tasks.
*/
class Scheduler implements SingletonInterface
{
/**
* @var array $extConf Settings from the extension manager
*/
public array $extConf = [];
/**
* Constructor, makes sure all derived client classes are included
*/
public function __construct(
protected readonly LoggerInterface $logger,
protected readonly TaskSerializer $taskSerializer,
protected readonly SchedulerTaskRepository $schedulerTaskRepository,
protected readonly EventDispatcherInterface $eventDispatcher,
protected readonly Registry $registry,
protected readonly ConnectionPool $connectionPool,
ExtensionConfiguration $extensionConfiguration,
) {
// Get configuration from the extension manager
$this->extConf = $extensionConfiguration->get('scheduler');
if (empty($this->extConf['maxLifetime'])) {
$this->extConf['maxLifetime'] = 1440;
}
// Clean up the serialized execution arrays
$this->cleanExecutionArrays();
}
/**
* Cleans the execution lists of the scheduled tasks, executions older than 24h are removed
* @todo find a way to actually kill the job
*/
protected function cleanExecutionArrays()
{
$tstamp = $GLOBALS['EXEC_TIME'];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_scheduler_task');
// Select all tasks with executions
// NOTE: this cleanup is done for disabled tasks too,
// to avoid leaving old executions lying around
$result = $queryBuilder->select('*')
->from('tx_scheduler_task')
->where(
$queryBuilder->expr()->neq(
'serialized_executions',
$queryBuilder->createNamedParameter('')
),
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
)
->executeQuery();
$maxDuration = $this->extConf['maxLifetime'] * 60;
while ($row = $result->fetchAssociative()) {
$executions = [];
// serialized in \TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository::addExecutionToTask as `array<int, int>`
if ($serialized_executions = unserialize($row['serialized_executions'], ['allowed_classes' => false])) {
foreach ($serialized_executions as $task) {
if ($tstamp - $task < $maxDuration) {
$executions[] = $task;
} else {
try {
$schedulerTask = $this->taskSerializer->deserialize($row);
$taskType = $schedulerTask->getTaskType();
$executionTime = date('Y-m-d H:i:s', $schedulerTask->getExecutionTime());
} catch (InvalidTaskException $e) {
$taskType = 'unknown type';
$executionTime = 'unknown time';
}
$this->logger->info(
'Removing logged execution, assuming that the process is dead. Execution of \'{taskType} \' (UID: {taskId}) was started at {executionTime}',
[
'taskType' => $taskType,
'taskId' => $row['uid'],
'executionTime' => $executionTime,
]
);
}
}
}
$executionCount = count($executions);
if (!is_array($serialized_executions) || count($serialized_executions) !== $executionCount) {
if ($executionCount === 0) {
$value = '';
} else {
$value = serialize($executions);
}
$this->connectionPool->getConnectionForTable('tx_scheduler_task')->update(
'tx_scheduler_task',
['serialized_executions' => $value],
['uid' => (int)$row['uid']],
['serialized_executions' => Connection::PARAM_LOB]
);
}
}
}
/**
* This method executes the given task and properly marks and records that execution
* It is expected to return FALSE if the task was barred from running or if it was not saved properly
*
* @param Task\AbstractTask $task The task to execute
* @return bool Whether the task was saved successfully to the database or not
* @throws \Throwable
*/
public function executeTask(AbstractTask $task): bool
{
$task->setRunOnNextCronJob(false);
// Trigger the saving of the task, as this will calculate its next execution time
// This should be calculated all the time, even if the execution is skipped
// (in case it is skipped, this pushes back execution to the next possible date)
$this->schedulerTaskRepository->updateExecution($task, $task->getExecution()->isSingleRun());
// Reserve an id for the upcoming execution
$executionID = $this->schedulerTaskRepository->addExecutionToTask($task);
// Make sure we're the only one executing a single-execution-only task
if (!$task->getExecution()?->isParallelExecutionAllowed() && $executionID > 0) {
$this->schedulerTaskRepository->removeExecutionOfTask($task, $executionID);
$this->logger->info('Task is already running and multiple executions are not allowed, skipping! Task Type: {taskType}, UID: {uid}', [
'taskType' => $task->getTaskType(),
'uid' => $task->getTaskUid(),
]);
return false;
}
// Log scheduler invocation
$this->logger->info('Start execution. Task Type: {taskType}, UID: {uid}', [
'taskType' => $task->getTaskType(),
'uid' => $task->getTaskUid(),
]);
$failureString = '';
$success = false;
$e = null;
try {
// Execute task
$successfullyExecuted = $task->execute();
if (!$successfullyExecuted) {
throw new FailedExecutionException('Task failed to execute successfully. Task Type: ' . $task->getTaskType() . ', UID: ' . $task->getTaskUid(), 1250596541);
}
$success = true;
return true;
} catch (\Throwable $e) {
// Log failed execution
$this->logger->error('Task failed to execute successfully. Task Type: {taskType}, UID: {taskId}, Code: {code}, "{message}" in {exceptionFile} at line {exceptionLine}', [
'taskType' => $task->getTaskType(),
'taskId' => $task->getTaskUid(),
'exception' => $e,
'exceptionFile' => $e->getFile(),
'exceptionLine' => $e->getLine(),
'code' => $e->getCode(),
'message' => $e->getMessage(),
]);
// Store exception, so that it can be saved to database
// Do not serialize the complete exception or the trace, this can lead to huge strings > 50MB
$failureString = serialize([
'code' => $e->getCode(),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'traceString' => $e->getTraceAsString(),
]);
// Now that the result of the task execution has been handled,
// throw the exception again, if any
throw $e;
} finally {
// Un-register execution
$this->schedulerTaskRepository->removeExecutionOfTask($task, $executionID, $failureString);
// Log completion of execution
$this->logger->info('Task executed. Task Type: {taskType}, UID: {uid}', [
'taskType' => $task->getTaskType(),
'uid' => $task->getTaskUid(),
]);
$this->eventDispatcher->dispatch(
new AfterTaskExecutionEvent($task, $success, $e)
);
}
}
/**
* This method stores information about the last run of the Scheduler into the system registry
*
* @param string $type Type of run (manual or command-line (assumed to be cron))
*/
public function recordLastRun($type = 'cron')
{
// Validate input value
if ($type !== 'manual' && $type !== 'cli-by-id') {
$type = 'cron';
}
$runInformation = ['start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type];
$this->registry->set('tx_scheduler', 'lastRun', $runInformation);
}
}