Fassung 1.6.0: der Aufgabenplan wird veroeffentlicht
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:
1 parent
d1ac9d760b
commit
05ea703655
35 files changed
+2229
-13
No files matched your search
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Ops\Scheduler;
|
||||
use App\Ops\Updater;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\TerminateEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
/**
|
||||
* Der Auslöser, den ein Hosting ohne Cronjob hat: die Anfrage selbst.
|
||||
*
|
||||
* Nach `kernel.terminate`, also nachdem die Antwort das Haus verlassen hat. Unter PHP-FPM ruft
|
||||
* Symfony dafür `fastcgi_finish_request()` auf - der Browser ist fertig, während hier noch
|
||||
* gearbeitet wird. Unter Apache mit mod_php gibt es das nicht: die Seite steht dann zwar schon
|
||||
* im Browser, der Prozess bleibt aber belegt, bis die Aufgabe fertig ist. Deshalb sind die
|
||||
* Grenzen klein - zwei Aufgaben, zwanzig Sekunden -, und deshalb lässt sich dieser Auslöser
|
||||
* abschalten, wenn ein echter Cronjob die Arbeit übernimmt.
|
||||
*
|
||||
* Der Normalfall kostet nichts: eine Datei lesen, Zeitpunkt vergleichen, fertig. Erst wenn
|
||||
* wirklich etwas ansteht, wird die Datenbank gefragt.
|
||||
*/
|
||||
final class TaskWorkerSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/** Anfragen, hinter denen kein Mensch und keine Seite steht. */
|
||||
private const IGNORED = '#^/(assets/|media/|favicon)#';
|
||||
|
||||
/** Ein Aufruf, der die Aufgaben schon selbst abgearbeitet hat, setzt dieses Attribut. */
|
||||
public const HANDLED = 'kant_tasks_handled';
|
||||
|
||||
public function __construct(
|
||||
private Scheduler $scheduler,
|
||||
private string $projectDir,
|
||||
private string $adminPath,
|
||||
private int $limit = 2,
|
||||
private int $budget = 20,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
// Nach dem Zähler der Statistik (Standardpriorität 0): der schreibt eine Zeile, das hier
|
||||
// kann Sekunden dauern.
|
||||
return [KernelEvents::TERMINATE => ['onTerminate', -64]];
|
||||
}
|
||||
|
||||
public function onTerminate(TerminateEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
if ($request->attributes->getBoolean(self::HANDLED)) {
|
||||
return;
|
||||
}
|
||||
$path = $request->getPathInfo();
|
||||
if (1 === preg_match(self::IGNORED, $path)) {
|
||||
return;
|
||||
}
|
||||
// Während eines Updates läuft nichts: die Dateien sind vielleicht schon die neuen und
|
||||
// die Datenbank noch die alte. Und vor der Installation gibt es keine Tabelle.
|
||||
if (is_file($this->projectDir.'/'.Updater::MAINTENANCE_FILE) || !is_file($this->projectDir.'/'.InstallSubscriber::MARKER)) {
|
||||
return;
|
||||
}
|
||||
$admin = str_starts_with($path, '/'.$this->adminPath);
|
||||
$state = $this->scheduler->fastState();
|
||||
if (!($admin ? $state['admin'] : $state['traffic'])) {
|
||||
return;
|
||||
}
|
||||
if (null !== $state['next'] && $state['next'] > time()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Die Antwort ist raus; ein abgebrochener Aufruf soll die Arbeit trotzdem zu Ende
|
||||
// bringen, sonst bliebe eine Aufgabe auf halbem Weg mit gesetzter Sperre liegen.
|
||||
@ignore_user_abort(true);
|
||||
@set_time_limit($this->budget + 60);
|
||||
$this->scheduler->runDue($admin ? 'verwaltung' : 'besuch', $this->limit, $this->budget);
|
||||
} catch (\Throwable) {
|
||||
// Eine Aufgabe darf niemals eine Website kaputtmachen. Was schiefging, steht in der
|
||||
// Zeile der Aufgabe und in var/tasks.log - hier ist niemand mehr, dem man es sagen
|
||||
// könnte: die Antwort ist längst verschickt.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user