From b36dceb48667578e8089364088b47bd8705b47db Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Wed, 11 Nov 2026 11:21:53 +0100 Subject: [PATCH] Scheduler: VITEC tasks instead of separate crontab lines New command vitec:scheduler-setup creates the scheduler group VITEC with the tasks vitec:ssr-health every 5 minutes and vitec:client-error-cleanup daily at 04:23, safe to run again; --disabled creates them switched off, --enable switches them on after the old crontab lines are gone. Co-Authored-By: Claude Opus 5.5 --- .../Classes/Command/SchedulerSetupCommand.php | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 packages/vitec/Classes/Command/SchedulerSetupCommand.php diff --git a/packages/vitec/Classes/Command/SchedulerSetupCommand.php b/packages/vitec/Classes/Command/SchedulerSetupCommand.php new file mode 100644 index 0000000..7a21f4d --- /dev/null +++ b/packages/vitec/Classes/Command/SchedulerSetupCommand.php @@ -0,0 +1,136 @@ + [cron expression, description] */ + private const TASKS = [ + 'vitec:ssr-health' => [ + '*/5 * * * *', + 'SSR health check: samples /__health and /__stats of the SSR node into the health log (cockpit charts), ' + . 'mails the alert recipients from Site settings › VITEC on down/up changes and falls back to the SPA ' + . 'after repeated failures. Formerly crontab.', + ], + 'vitec:client-error-cleanup' => [ + '23 4 * * *', + 'Deletes browser error reports (client errors from the React frontend) older than 30 days. Formerly crontab.', + ], + ]; + + public function __construct( + private readonly TaskService $taskService, + private readonly SchedulerTaskRepository $taskRepository, + private readonly ConnectionPool $connectionPool, + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this->addOption('disabled', null, InputOption::VALUE_NONE, 'Create new tasks switched off'); + $this->addOption('enable', null, InputOption::VALUE_NONE, 'Switch on all VITEC tasks created by this command'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + Bootstrap::initializeBackendAuthentication(); + $groupUid = $this->groupUid(); + $output->writeln('Scheduler group "' . self::GROUP . '": uid ' . $groupUid); + + foreach (self::TASKS as $command => [$cron, $description]) { + $existing = $this->findTask($command); + if ($existing !== null) { + if ($input->getOption('enable') && (int)$existing['disable'] === 1) { + $task = $this->taskRepository->findByUid((int)$existing['uid']); + $task->setDisabled(false); + $this->taskRepository->update($task); + $output->writeln(sprintf('%s: enabled (uid %d)', $command, $existing['uid'])); + } else { + $output->writeln(sprintf('%s: exists (uid %d, %s)', $command, $existing['uid'], + (int)$existing['disable'] === 1 ? 'disabled' : 'enabled')); + } + continue; + } + if (!$this->taskService->isTaskTypeRegistered($command)) { + $output->writeln('' . $command . ': not available as scheduler task type'); + continue; + } + $task = $this->taskService->createNewTask($command); + $task->setDescription($description); + $task->setTaskGroup($groupUid); + $task->setExecution(Execution::createRecurringExecution(time() - 60, 0, 0, false, $cron)); + $task->setDisabled((bool)$input->getOption('disabled')); + if ($this->taskRepository->add($task)) { + $output->writeln(sprintf('%s: created (uid %d, %s, %s)', $command, $task->getTaskUid(), $cron, + $task->isDisabled() ? 'disabled' : 'enabled')); + } else { + $output->writeln('' . $command . ': could not be created'); + } + } + return Command::SUCCESS; + } + + private function groupUid(): int + { + $connection = $this->connectionPool->getConnectionForTable('tx_scheduler_task_group'); + $uid = $connection->executeQuery( + 'SELECT uid FROM tx_scheduler_task_group WHERE groupName = ? AND deleted = 0 ORDER BY uid LIMIT 1', + [self::GROUP] + )->fetchOne(); + if ($uid !== false) { + return (int)$uid; + } + $sorting = (int)$connection->executeQuery('SELECT MAX(sorting) FROM tx_scheduler_task_group')->fetchOne(); + $connection->insert('tx_scheduler_task_group', [ + 'pid' => 0, + 'tstamp' => time(), + 'crdate' => time(), + 'groupName' => self::GROUP, + 'color' => '#f47937', + 'description' => 'Recurring jobs of the VITEC website (SSR monitoring, cleanups)', + 'sorting' => $sorting + 256, + ], ['pid' => Connection::PARAM_INT, 'tstamp' => Connection::PARAM_INT, 'crdate' => Connection::PARAM_INT, 'sorting' => Connection::PARAM_INT]); + return (int)$connection->lastInsertId(); + } + + private function findTask(string $command): ?array + { + $row = $this->connectionPool->getConnectionForTable('tx_scheduler_task')->executeQuery( + 'SELECT uid, disable FROM tx_scheduler_task WHERE tasktype = ? AND deleted = 0 ORDER BY uid LIMIT 1', + [$command] + )->fetchAssociative(); + return $row === false ? null : $row; + } +}