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>
67 lines
3.5 KiB
PHP
67 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace DoctrineMigrations;
|
|
|
|
use Doctrine\DBAL\Schema\Schema;
|
|
use Doctrine\Migrations\AbstractMigration;
|
|
|
|
final class Version20260914000100 extends AbstractMigration
|
|
{
|
|
public function getDescription(): string
|
|
{
|
|
return 'Der Aufgabenplan: eine Zeile je wiederkehrender Aufgabe, mit Faelligkeit, Sperre und letztem Ergebnis.';
|
|
}
|
|
|
|
public function isTransactional(): bool
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public function up(Schema $schema): void
|
|
{
|
|
// Warum eine Tabelle und keine Datei: die Sperre. Zwei Besucher koennen im selben
|
|
// Moment dieselbe Aufgabe faellig sehen, und nur einer darf sie ausfuehren. Ein
|
|
// bedingtes UPDATE ist auf MySQL wie auf SQLite atomar; zwei Prozesse, die eine Datei
|
|
// sperren, sind es auf geteiltem Hosting nicht zuverlaessig.
|
|
//
|
|
// Die Tabelle bleibt klein - eine Zeile je registrierter Aufgabe, heute fuenf. Deshalb
|
|
// steht hier kein Index: ein voller Durchlauf ueber fuenf Zeilen ist billiger als ein
|
|
// Index, den jede Aenderung mitpflegen muss.
|
|
$tasks = $schema->createTable('scheduled_task');
|
|
// Der technische Name aus dem Code, zum Beispiel `publish-due`. Er ist der Schluessel:
|
|
// verschwindet eine Aufgabe aus dem Code, verschwindet ihre Zeile beim naechsten Abgleich.
|
|
$tasks->addColumn('name', 'string', ['length' => 64]);
|
|
// Der Abstand zwischen zwei Laeufen. Kommt als Vorgabe aus dem Code und ist in der
|
|
// Verwaltung aenderbar, weil eine taegliche Sicherung auf der einen Installation
|
|
// richtig und auf der naechsten zu selten ist.
|
|
$tasks->addColumn('interval_seconds', 'integer', ['default' => 3600]);
|
|
$tasks->addColumn('enabled', 'boolean', ['default' => true]);
|
|
// Alle Zeitpunkte in UTC, als Zeichenkette verglichen - wie ueberall sonst im Projekt.
|
|
$tasks->addColumn('next_run_at', 'datetime_immutable');
|
|
$tasks->addColumn('last_run_at', 'datetime_immutable', ['notnull' => false]);
|
|
// Bis wann diese Aufgabe als "laeuft gerade" gilt. Danach darf sie ein anderer
|
|
// Durchlauf uebernehmen: ein Prozess, den das Hosting nach 30 Sekunden abgeschossen
|
|
// hat, kann seine Sperre nicht mehr selbst aufraeumen.
|
|
$tasks->addColumn('locked_until', 'datetime_immutable', ['notnull' => false]);
|
|
$tasks->addColumn('last_status', 'string', ['length' => 16, 'notnull' => false]);
|
|
$tasks->addColumn('last_message', 'string', ['length' => 500, 'default' => '']);
|
|
$tasks->addColumn('last_duration_ms', 'integer', ['default' => 0]);
|
|
// Wodurch der letzte Lauf ausgeloest wurde: besuch, verwaltung, cron, konsole, hand.
|
|
// Steht hier, weil die erste Frage bei einer Stoerung immer dieselbe ist: laeuft der
|
|
// Ausloeser ueberhaupt, den der Betreiber eingerichtet zu haben glaubt?
|
|
$tasks->addColumn('last_trigger', 'string', ['length' => 16, 'default' => '']);
|
|
// Aufeinanderfolgende Fehlschlaege. Sie verlaengern den Abstand bis zum naechsten
|
|
// Versuch, damit eine Aufgabe, die nicht laufen kann, nicht bei jedem Seitenaufruf
|
|
// wieder anfaengt.
|
|
$tasks->addColumn('failures', 'integer', ['default' => 0]);
|
|
$tasks->setPrimaryKey(['name']);
|
|
}
|
|
|
|
public function down(Schema $schema): void
|
|
{
|
|
// Faellt die Tabelle, faellt nur der Plan: Zeitpunkte und letzte Meldungen. Kein Inhalt,
|
|
// kein Konto, keine Datei. Beim naechsten Start legt der Abgleich sie neu an.
|
|
$schema->dropTable('scheduled_task');
|
|
}
|
|
}
|