Forms gained steps, conditions, image choices, number fields with units and a summary step; the DEEG price finder uses all of it. Plugins are manifest plus Twig: blocks, settings, containers, routes, presets and theme packs, switched per installation. The review backlog is done: archive pages render through the normal page pipeline, forms and post lists work inside areas and sections, repeated sections get their own ids, breakpoints and colours come from the palette, backup and update work on MySQL and lock each other out. The task worker no longer shortens a process that has no time limit: set_time_limit() sets the limit instead of extending it, which turned an unlimited CLI run into eighty seconds and killed the full test suite far away from the scheduler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
4.1 KiB
PHP
101 lines
4.1 KiB
PHP
<?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]];
|
|
}
|
|
|
|
/**
|
|
* Hebt die Laufzeitgrenze an - und senkt sie nie.
|
|
*
|
|
* `set_time_limit()` setzt die Grenze, es verlaengert nicht: auf einem Prozess ohne Grenze
|
|
* (Konsole, Testlauf, Hoster mit `max_execution_time = 0`) macht ein fester Wert aus
|
|
* "unbegrenzt" plötzlich "noch so viele Sekunden" - und der Prozess stirbt spaeter an einer
|
|
* Stelle, die mit Aufgaben nichts zu tun hat.
|
|
*/
|
|
private function raiseTimeLimit(int $seconds): void
|
|
{
|
|
$current = (int) ini_get('max_execution_time');
|
|
if ($current > 0 && $current < $seconds) {
|
|
@set_time_limit($seconds);
|
|
}
|
|
}
|
|
|
|
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);
|
|
$this->raiseTimeLimit($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.
|
|
}
|
|
}
|
|
}
|