CluPilotCloud/app/Jobs/PacedMail.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);
}
}