Fix-Runde 2: eine fremde Mailklasse faehrt im ruhigen Takt statt ungebremst

gedrosselteSpur() las die Spur nur aus $this->mailable->queue. Die setzt aber
allein RidesALane, und den tragen ausschliesslich unsere vierzehn Klassen. Eine
Mailklasse aus einem Paket — Fortify, ein kuenftiges Abhaengigkeitspaket —
bekam damit gar keine Drossel.

Das ist die Gegenrichtung zu der Entscheidung aus Aufgabe 1: dort faellt eine
unbekannte Klasse ausdruecklich in die ruhige Spur, weil eine zu langsam
verschickte Mail ein kleinerer Fehler ist als ein ungedrosselter Schub, den
niemand vorhergesehen hat. Steht keine Spur am Mailable, entscheidet jetzt
MailLane::for() — dieselbe Quelle, die Aufgabe 1 dafuer gebaut hat.

Schlange und Takt fallen fuer so eine Mail auseinander: sie bleibt auf default,
weil die Schlange der Trait waehlt, und faehrt trotzdem im ruhigen Takt, weil
den der Auftrag waehlt. Der Kommentar sagt, warum das kein Versehen ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/versandtakt
nexxo 2026-08-03 19:04:32 +02:00
parent e99da32aa2
commit 96bc2c5a07
2 changed files with 71 additions and 12 deletions

View File

@ -99,19 +99,31 @@ class PacedMail extends SendQueuedMailable
return null;
}
// 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äse die Einstellungen erst im Arbeiter: 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.
// 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.
//
// `?? 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) {
// 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,

View File

@ -105,6 +105,29 @@ class EigeneGrenzeMail extends Mailable implements ShouldQueue
}
}
/**
* Eine Mailklasse aus der Fremde: kein Trait, keine Spur am Mailable.
*
* So sieht jede Mail aus, die nicht aus `app/Mail` kommt Fortify, ein
* künftiges Abhängigkeitspaket, irgendetwas, das jemand einbindet. Sie kennt
* `RidesALane` nicht, also steht in `$mailable->queue` nichts, und sie liegt
* auf `default`.
*/
class FremdeMail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function envelope(): Envelope
{
return new Envelope(from: 'takt@example.test', subject: 'Fremd');
}
public function content(): Content
{
return new Content(htmlString: '<p>Fremd</p>');
}
}
/**
* Der Auftrag, so wie ihn das Einreihen erzeugt.
*
@ -323,6 +346,30 @@ it('hängt jede gedrosselte Spur an ihre eigene Drossel', function (string $spur
->and(drosselName($middleware[0]))->toBe($spur);
})->with([MailLane::URGENT, MailLane::CALM]);
/**
* Eine fremde Mailklasse fährt im ruhigen Takt, nicht ungebremst.
*
* Aufgabe 1 hat sich für diese Richtung entschieden: eine unbekannte Klasse
* fällt in die ruhige Spur, weil eine zu langsam verschickte Mail ein kleinerer
* Fehler ist als ein ungedrosselter Schub, den niemand vorhergesehen hat. Am
* Auftrag fiel sie bis hierher in die Gegenrichtung ohne Spur am Mailable
* griff gar keine Drossel.
*
* Schlange und Takt fallen dabei auseinander, und das ist kein Versehen: die
* Schlange wählt der Trait, den diese Klasse nicht hat, den Takt wählt der
* Auftrag. Sie liegt also weiter auf `default` und fährt trotzdem im ruhigen
* Takt.
*/
it('drosselt eine Mailklasse ohne den Trait wie die ruhige Spur', function () {
$auftrag = taktAuftrag(new FremdeMail);
$middleware = $auftrag->middleware();
expect($auftrag->mailable->queue)->toBeNull()
->and($middleware)->toHaveCount(1)
->and(drosselName($middleware[0]))->toBe(MailLane::CALM)
->and($auftrag->retryUntil())->not->toBeNull();
});
/**
* Auf der direkten Spur wartet gerade ein Mensch, der eben geklickt hat.
* Drosseln nützt dort nichts die Mails entstehen einzeln und können gar