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` 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); } }