technik: Aufrufe über ein Ereignis-Log zählen statt im Request

Bisher lief pro Seitenaufruf ein UPDATE press_releases SET hits = hits + 1 im
Request. Auf der über 1 GB großen Tabelle blockiert dieser Schreibzugriff,
sobald dort eine Schemaänderung läuft — die wartenden Requests belegten dabei
PHP-FPM-Worker und DB-Verbindungen, bis die ganze Seite stand.

Der Request schreibt jetzt nur noch in die kleine Append-Tabelle
press_release_events, und selbst dabei wird jeder Fehler geschluckt und
gemeldet: Ein Aufruf-Zähler darf eine Seite niemals langsamer oder kaputt
machen. Die Summierung in press_releases.hits übernimmt gebündelt der
Scheduler.

Der Fortschritt läuft über eine Wasserstandsmarke (höchste bereits verbuchte
Event-ID) statt über einen Zwischenspeicher, damit bei einem Neustart kein
Aufruf verloren geht. Zähler und Marke werden je Durchgang gemeinsam
committet — sonst wäre nur der Durchgang wiederanlaufsicher, nicht der Abbruch
darin, und der nächste Lauf verbuchte dieselben Aufrufe doppelt. Eine Migration
setzt die Marke beim Deploy auf den Ist-Stand; ohne sie würde der erste Lauf
den gesamten Altbestand ein zweites Mal aufaddieren.

Weil das Log damit mit dem Traffic wächst und als einzige Log-Tabelle des
Systems keine Bereinigung hatte, begrenzt press-releases:purge-events es auf
einen Aufbewahrungszeitraum (Default 395 Tage = 13 Monate, damit der
Vorjahresmonats-Vergleich möglich bleibt). Gelöscht wird ausschließlich
unterhalb der Wasserstandsmarke — alles darüber ist noch nicht in hits verbucht
und wäre sonst dauerhaft verloren. Fehlt die Marke, bricht der Lauf ab, statt
zu raten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-01 11:05:28 +00:00
parent f8728c5bca
commit 6a34ab48ee
11 changed files with 565 additions and 12 deletions

View file

@ -0,0 +1,50 @@
<?php
use App\Console\Commands\AggregatePressReleaseViews;
use App\Models\AppSetting;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
/**
* Setzt die Wasserstandsmarke des Aufruf-Zählers auf den aktuellen Stand des
* Ereignis-Logs.
*
* Zwingend nötig: `press_releases.hits` enthält bereits alle bisherigen
* Aufrufe (inklusive Legacy-Bestand). Ohne diese Marke würde der erste Lauf
* von `press-releases:aggregate-views` sämtliche Alt-Ereignisse (im Bestand
* knapp eine Million) ein zweites Mal auf die Zähler addieren.
*
* Auf einer frischen Installation ist das Log leer, die Marke bleibt 0 — dann
* werden korrekterweise alle Ereignisse ab dem ersten verbucht.
*/
return new class extends Migration
{
public function up(): void
{
$latestEventId = (int) DB::table('press_release_events')->max('id');
DB::table('app_settings')->updateOrInsert(
['key' => AggregatePressReleaseViews::WatermarkKey],
[
'value' => json_encode($latestEventId),
'created_at' => now(),
'updated_at' => now(),
],
);
// Hier wird am Model vorbei geschrieben, `AppSetting::getValue()` cacht
// aber dauerhaft. Lief der Scheduler-Task im Deploy-Fenster schon
// einmal, hielte der Cache die alte, niedrigere Marke — der nächste
// Lauf zählte die Ereignisse dazwischen ein zweites Mal auf `hits`.
AppSetting::forgetCached(AggregatePressReleaseViews::WatermarkKey);
}
public function down(): void
{
DB::table('app_settings')
->where('key', AggregatePressReleaseViews::WatermarkKey)
->delete();
AppSetting::forgetCached(AggregatePressReleaseViews::WatermarkKey);
}
};