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 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Opus 5.5 committed 2026-11-11 11:21:53 +01:00
1 parent 90cb2faaf9
commit b36dceb486
1 file changed
+136
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
use TYPO3\CMS\Scheduler\Execution;
use TYPO3\CMS\Scheduler\Service\TaskService;
/**
* Puts the project's recurring jobs into the TYPO3 Scheduler (group
* "VITEC"), so they are visible, startable and switchable in
* System › Scheduler instead of hiding in the crontab. The only cron
* line left on dedi5092 is the core "scheduler:run" every minute.
*
* Idempotent: a task whose command already exists in the scheduler is
* left alone. --disabled creates new tasks switched off (use while the
* old crontab lines still run, so nothing runs twice), --enable switches
* the VITEC tasks on afterwards.
*/
#[AsCommand(
name: 'vitec:scheduler-setup',
description: 'Create the VITEC scheduler tasks (SSR health every 5 min, client error cleanup daily) in the group "VITEC"; safe to run again'
)]
final class SchedulerSetupCommand extends Command
{
private const GROUP = 'VITEC';
/** command => [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('<info>%s: enabled (uid %d)</info>', $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('<error>' . $command . ': not available as scheduler task type</error>');
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('<info>%s: created (uid %d, %s, %s)</info>', $command, $task->getTaskUid(), $cron,
$task->isDisabled() ? 'disabled' : 'enabled'));
} else {
$output->writeln('<error>' . $command . ': could not be created</error>');
}
}
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;
}
}