TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user