133 lines
6.2 KiB
PHP
133 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Services\Mail\MailLane;
|
|
use App\Support\Settings;
|
|
use Illuminate\Mail\SendQueuedMailable;
|
|
use Illuminate\Queue\Middleware\RateLimited;
|
|
|
|
/**
|
|
* Der Auftrag, der eine Mail im Takt ihrer Spur verschickt.
|
|
*
|
|
* `SendQueuedMailable` selbst hat keine `middleware()` — nachgezählt im
|
|
* Framework: handle, backoff, retryUntil, failed, displayName, __clone. Der
|
|
* eigene Auftrag ist trotzdem nicht der einzige Weg: eine `middleware()` auf
|
|
* der MAILKLASSE wird sehr wohl gelesen, weil `Mailable::newQueuedJob()` sie
|
|
* einsammelt und per `->through()` an den Auftrag weiterreicht.
|
|
*
|
|
* Der Takt sitzt hier statt im Trait, weil er dann auch für Mailklassen greift,
|
|
* die den Trait NICHT benutzen (Pakete, Fortify) — und weil er sich mit einer
|
|
* Mailable-eigenen `middleware()` verträgt, statt sie zu verdrängen: die
|
|
* Bindung im Container tauscht den Auftrag für jede Mail aus, ohne dass eine
|
|
* einzige Absendestelle sich ändert.
|
|
*
|
|
* Zur Falle mit den Versuchen: der Arbeiter läuft mit --tries=3, und eine
|
|
* gedrosselte Rückstellung zählt als Versuch. Deshalb `retryUntil()` statt
|
|
* eines Versuchszählers — aber nur dort, wo wirklich gedrosselt wird.
|
|
*/
|
|
class PacedMail extends SendQueuedMailable
|
|
{
|
|
/** @return array<int, object> */
|
|
public function middleware(): array
|
|
{
|
|
$spur = $this->gedrosselteSpur();
|
|
|
|
return $spur === null ? [] : [new RateLimited($spur)];
|
|
}
|
|
|
|
/**
|
|
* Sechs Stunden statt eines Versuchszählers — auf den gedrosselten Spuren.
|
|
*
|
|
* Der Arbeiter prüft `retryUntil` VOR `--tries`: liegt sie in der Zukunft,
|
|
* steigt er aus, bevor er `attempts()` gegen die Versuchszahl hält. Genau
|
|
* das wird hier gebraucht, denn ein Zähler könnte nicht zwischen „dreimal
|
|
* zurückgelegt" und „dreimal gescheitert" unterscheiden — die Rechnung, die
|
|
* am Abend an der Drossel ansteht, wäre nach dem dritten Mal endgültig
|
|
* gescheitert statt verschickt.
|
|
*
|
|
* Und genau deshalb hängt die Grenze an derselben Bedingung wie die
|
|
* Drossel. Ein Zeitfenster hebelt `--tries` nicht bloß für Rückstellungen
|
|
* aus, sondern GANZ: `Worker::markJobAsFailedIfWillExceedMaxAttempts`
|
|
* prüft die Versuchszahl ausdrücklich nur `if (! $job->retryUntil())`. Wo
|
|
* nie gedrosselt wird — Direktspur, Notschalter aus —, gibt es keine
|
|
* Rückstellungen abzufangen, wohl aber eine Mail, die aus echtem Grund
|
|
* wirft: die würde sechs Stunden lang wiederholt statt nach drei Versuchen
|
|
* abgelegt. Ohne Pause obendrein, weil der Arbeiter ohne `--backoff` läuft
|
|
* und `Worker::calculateBackoff` dann auf null Sekunden fällt. Ihr Fehler
|
|
* stünde sechs Stunden lang nicht in `failed_jobs` und wäre für jede
|
|
* Alarmierung nicht vorhanden; auf der Direktspur zöge der Arbeiter sie
|
|
* obendrein bei jedem Durchlauf zuerst — sechs Stunden lang keine
|
|
* Kennwort-Zurücksetzung.
|
|
*
|
|
* Sechs Stunden ist weit genug, dass auch ein Lauf über die ganze Nacht
|
|
* durchkommt, und eng genug, dass eine Mail, die dann noch nicht draußen
|
|
* ist, nicht am nächsten Tag zwischen den neuen auftaucht.
|
|
*
|
|
* Ohne eigene Rückgabeangabe, wie die Elternklasse: die lässt neben einem
|
|
* Zeitpunkt auch einen Zeitstempel zu (`Queue::getJobExpiration` behandelt
|
|
* beides), und eine engere Angabe hier zerbräche eine Mailklasse, die von
|
|
* diesem Spielraum Gebrauch macht.
|
|
*
|
|
* @return \DateTimeInterface|int|null
|
|
*/
|
|
public function retryUntil()
|
|
{
|
|
// Die Mailklasse zuerst: die Elternklasse reicht an
|
|
// `$this->mailable->retryUntil` weiter. Heute deklariert das keine der
|
|
// vierzehn — aber eine Überschreibung, die nicht zurückfragt, nähme
|
|
// der nächsten ihre Entscheidung still weg.
|
|
if (($eigene = parent::retryUntil()) !== null) {
|
|
return $eigene;
|
|
}
|
|
|
|
return $this->gedrosselteSpur() === null ? null : now()->addHours(6);
|
|
}
|
|
|
|
/**
|
|
* Die Spur, deren Drossel für diesen Auftrag gilt — oder null.
|
|
*
|
|
* Eine Stelle für beide Fragen, weil `middleware()` und `retryUntil()`
|
|
* sonst auseinanderlaufen könnten. Beide Richtungen tun weh: ein
|
|
* Zeitfenster ohne Drossel nimmt die Versuchszahl weg, ohne etwas dafür zu
|
|
* geben, und eine Drossel ohne Zeitfenster ist genau die Falle, um die es
|
|
* in dieser Aufgabe geht.
|
|
*/
|
|
private function gedrosselteSpur(): ?string
|
|
{
|
|
if (! Settings::bool('mail.pace.enabled', true)) {
|
|
return null;
|
|
}
|
|
|
|
// Zuerst die Schlange am Mailable: das ist der Name, den
|
|
// `Mailable::queue()` an `pushOn()` gegeben hat — die Spur, in der
|
|
// dieser Auftrag TATSÄCHLICH liegt. `MailLane::for(...)` allein wäre
|
|
// hier falsch, weil es die Einstellungen erst im Arbeiter läse: würde
|
|
// der Betreiber die Rechnung von ruhig auf wichtig schieben, während
|
|
// zweihundert davon in der ruhigen Schlange warten, drosselte sie ab
|
|
// diesem Moment das falsche Kontingent.
|
|
//
|
|
// Steht dort nichts, kommt die Mail von auswärts — aus einem Paket,
|
|
// aus Fortify, aus irgendetwas, das jemand eingebunden hat. Sie kennt
|
|
// `RidesALane` nicht, hat also keine Spur und liegt auf `default`.
|
|
// Dann entscheidet `MailLane::for(...)`, und das ist Absicht: Aufgabe 1
|
|
// lässt eine unbekannte Klasse ausdrücklich in die ruhige Spur fallen,
|
|
// weil eine zu langsam verschickte Mail ein kleinerer Fehler ist als
|
|
// ein ungedrosselter Schub, den niemand vorhergesehen hat. Ohne diesen
|
|
// Rückgriff fiele sie am Auftrag in die Gegenrichtung.
|
|
//
|
|
// Schlange und Takt fallen für so eine Mail auseinander — sie bleibt
|
|
// auf `default` und fährt trotzdem im ruhigen Takt. Das ist kein
|
|
// Versehen, sondern die Arbeitsteilung: die Schlange wählt der Trait,
|
|
// den Takt wählt dieser Auftrag, und für eine fremde Mailklasse greift
|
|
// nur das zweite.
|
|
$spur = $this->mailable->queue ?? MailLane::for($this->mailable::class);
|
|
|
|
return match ($spur) {
|
|
MailLane::URGENT => MailLane::URGENT,
|
|
MailLane::CALM => MailLane::CALM,
|
|
default => null,
|
|
};
|
|
}
|
|
}
|