Fassung 1.6.0: der Aufgabenplan wird veroeffentlicht
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s

Wiederkehrende Aufgaben ohne Cronjob (ADR 073): Sicherung, Statistik kuerzen,
geplante Veroeffentlichung, Updatepruefung und Aufraeumen laufen als Plan in
der Tabelle scheduled_task - eine Zeile je Aufgabe mit Faelligkeit, Sperre und
letztem Ergebnis. Auf geteiltem Hosting gibt es oft keinen Cronjob; der Plan
haengt stattdessen an Anfragen und an einem Takt aus dem geoeffneten Backend.

Diese Arbeit entstand in einer parallel laufenden Sitzung und lag bis hierher
unveroeffentlicht im Arbeitsverzeichnis: die Releases 1.5.0 bis 1.5.5 wurden
bewusst aus dem jeweiligen Tag gebaut und enthielten sie deshalb nicht.

Vor der Veroeffentlichung geprueft, nicht bloss eingepackt:

- phpunit 452/452, npm test 28/28, npm run test:editor vollstaendig.
- Die Migration 20260914000100 legt nur die Tabelle scheduled_task an und nimmt
  sie beim Zurueckrollen wieder weg. Sie fasst keine bestehenden Daten an.
- Seit einer Stunde keine Schreibzugriffe der anderen Sitzung mehr.

Dabei zwei Fehler behoben:

- config/services.yaml verwendet !tagged_iterator. Das ist gueltiges Symfony,
  aber "lint:yaml config" kennt eigene Tags nur mit --parse-tags und gab 1
  zurueck - der CI-Lauf waere daran gescheitert. Flag ergaenzt.
- AdminHeadersSubscriber.php (aus 1.5.3) entsprach nicht dem Stil von
  php-cs-fixer; composer lint gab 8 zurueck. Behoben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-14 08:30:13 +02:00
1 parent d1ac9d760b
commit 05ea703655
35 files changed
+2229 -13

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
<?php
namespace App\Ops\Tasks;
use App\Ops\ArchiveUpdate;
use App\Ops\Scheduler;
use App\Ops\Updater;
/**
* Räumt in `var/` auf, was mit der Zeit liegen bleibt.
*
* Drei Dinge, und ausdrücklich nur diese drei: abgelaufene Sitzungsdateien, alte Arbeitsordner
* eines Updates und zu lang gewordene Protokolle. Nichts davon ist Inhalt, nichts davon lässt
* sich vermissen.
*
* Was diese Aufgabe **nicht** anfasst: Sicherungen (die rotiert `cms:backup` selbst und nach
* einer anderen Regel), den Cache (den baut Symfony auf, wenn er fehlt, aber ihn mitten im
* Betrieb zu löschen macht die nächste Anfrage langsam ohne Grund) und den Ordner eines Updates,
* das noch auf seinen zweiten Schritt wartet.
*/
final class CleanupTask implements Task
{
/** Sitzungen und Update-Arbeitsordner, die älter sind, werden entfernt. */
private const KEEP_DAYS = 30;
/** So viele Zeilen bleiben in den Protokollen stehen. */
private const KEEP_LINES = 200;
public function __construct(private string $projectDir)
{
}
public function name(): string
{
return 'cleanup';
}
public function label(): string
{
return 'Aufräumen';
}
public function explanation(): string
{
return sprintf(
'Entfernt Sitzungsdateien und Update-Arbeitsordner, die älter als %d Tage sind, und kürzt die Protokolle. Sicherungen und Inhalte bleiben unberührt.',
self::KEEP_DAYS,
);
}
public function interval(): int
{
return 86400;
}
public function run(): string
{
$limit = time() - self::KEEP_DAYS * 86400;
return sprintf(
'%d Sitzungsdatei(en), %d Update-Ordner entfernt, %d Protokoll(e) gekürzt.',
$this->sessions($limit),
$this->updateFolders($limit),
$this->logs(),
);
}
/** Sitzungsdateien. Der Garbage Collector von PHP ist auf vielen Hostings abgeschaltet. */
private function sessions(int $limit): int
{
$removed = 0;
foreach (glob($this->projectDir.'/var/sessions/*/sess_*') ?: [] as $file) {
if (is_file($file) && (int) @filemtime($file) < $limit && @unlink($file)) {
++$removed;
}
}
return $removed;
}
/**
* Die Arbeitsordner des Archiv-Updates.
*
* Der jüngste ist der Rückweg aus dem letzten Update, und den lässt diese Aufgabe einen
* Monat lang stehen. Wartet ein Update noch auf seinen zweiten Schritt, wird gar nichts
* angefasst: dann ist einer dieser Ordner der Stand, auf den zurückgegangen wird.
*/
private function updateFolders(int $limit): int
{
if (is_file($this->projectDir.'/'.ArchiveUpdate::MARKE)) {
return 0;
}
$removed = 0;
foreach (glob($this->projectDir.'/var/update/*', GLOB_ONLYDIR) ?: [] as $folder) {
if ((int) @filemtime($folder) < $limit && $this->removeDirectory($folder)) {
++$removed;
}
}
return $removed;
}
private function logs(): int
{
$shortened = 0;
foreach ([Updater::LOG_FILE, Scheduler::LOG_FILE] as $relative) {
$file = $this->projectDir.'/'.$relative;
$raw = @file_get_contents($file);
if (!is_string($raw)) {
continue;
}
$lines = array_values(array_filter(explode("\n", $raw)));
if (count($lines) <= self::KEEP_LINES) {
continue;
}
if (false !== @file_put_contents($file, implode("\n", array_slice($lines, -self::KEEP_LINES))."\n", LOCK_EX)) {
++$shortened;
}
}
return $shortened;
}
/** Rekursiv löschen, aber nur unterhalb von `var/` – geprüft, nicht angenommen. */
private function removeDirectory(string $path): bool
{
$real = realpath($path);
$var = realpath($this->projectDir.'/var');
if (false === $real || false === $var || !str_starts_with($real, $var.DIRECTORY_SEPARATOR)) {
return false;
}
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($real, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST,
);
foreach ($items as $item) {
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
}
return @rmdir($real);
}
}