Files
orascheandClaude Opus 5.5 b36dceb486 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>
2026-11-11 11:21:53 +01:00

137 lines
6.0 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}