72 lines
3.0 KiB
PHP
72 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Services\Mail\MailLane;
|
|
use App\Support\Settings;
|
|
use DateTimeInterface;
|
|
use Illuminate\Mail\SendQueuedMailable;
|
|
use Illuminate\Queue\Middleware\RateLimited;
|
|
|
|
/**
|
|
* Der Auftrag, der eine Mail im Takt ihrer Spur verschickt.
|
|
*
|
|
* `SendQueuedMailable` kennt keine `middleware()` — nachgesehen im Framework:
|
|
* die Klasse hat handle, backoff, retryUntil, failed, displayName, __clone und
|
|
* sonst nichts. Eine `middleware()` auf der MAILKLASSE liest deshalb niemand.
|
|
* Der Weg führt über diesen eigenen Auftrag, den `Mailable::newQueuedJob()`
|
|
* über den Container erzeugt — eine Bindung tauscht ihn für alle Mails 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 — eine zeitliche Grenze kennt keine Rückstellungen,
|
|
* sondern nur ein Ende.
|
|
*/
|
|
class PacedMail extends SendQueuedMailable
|
|
{
|
|
/** @return array<int, object> */
|
|
public function middleware(): array
|
|
{
|
|
if (! Settings::bool('mail.pace.enabled', true)) {
|
|
return [];
|
|
}
|
|
|
|
// Die Schlange am Mailable, nicht `MailLane::for(...)`: das hier ist
|
|
// der Name, den `Mailable::queue()` an `pushOn()` gegeben hat — die
|
|
// Spur, in der dieser Auftrag TATSÄCHLICH liegt. `MailLane::for(...)`
|
|
// läge zwischen Einreihen und Abarbeiten eine Zuordnung später: 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.
|
|
//
|
|
// `?? null` statt eines nackten Zugriffs, weil `$this->mailable` nur
|
|
// die Schnittstelle zusichert: eine Mailklasse, die den Trait nicht
|
|
// benutzt, hat die Eigenschaft womöglich gar nicht — und liegt dann
|
|
// auch in keiner Spur, gehört also nicht gedrosselt.
|
|
return match ($this->mailable->queue ?? null) {
|
|
MailLane::URGENT => [new RateLimited(MailLane::URGENT)],
|
|
MailLane::CALM => [new RateLimited(MailLane::CALM)],
|
|
default => [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sechs Stunden statt eines Versuchszählers.
|
|
*
|
|
* Der Arbeiter prüft `retryUntil` VOR `--tries`: liegt sie in der Zukunft,
|
|
* steigt er aus, bevor er `attempts()` gegen die Versuchszahl hält. Ein
|
|
* Zähler dagegen könnte nicht zwischen „dreimal zurückgelegt" und „dreimal
|
|
* gescheitert" unterscheiden — die Rechnung, die am Abend zweihundertmal an
|
|
* der Drossel ansteht, wäre nach dem dritten Mal endgültig gescheitert.
|
|
*
|
|
* Weit genug, dass auch ein Lauf über die ganze Nacht durchkommt, und eng
|
|
* genug, dass eine Mail, die nach sechs Stunden noch nicht draußen ist,
|
|
* nicht am nächsten Tag zwischen den neuen auftaucht.
|
|
*/
|
|
public function retryUntil(): DateTimeInterface
|
|
{
|
|
return now()->addHours(6);
|
|
}
|
|
}
|