Entwurf: der Versandtakt

Der Betreiber will nicht, dass zweihundert Rechnungen als Schub bei einem
Empfaengerserver ankommen und die Absenderadresse dort als Spam eingestuft wird.
Heute sind es elf Kunden und zwei Rechnungen — die Regel wird fuer den Zustand
gebaut, in dem sie fehlen wuerde.

Drei Spuren, rund um die Uhr, kein Zeitfenster: Direkt (ungedrosselt), Wichtig
(30 je 5 Minuten), Zeit lassen (20 je 10 Minuten). Die Trennung laeuft nicht
zwischen Massenversand und Einzelmail, wie ich zuerst annahm, sondern zwischen
dringend und nicht dringend — eine Ausfallmeldung geht an alle UND eilt, und
bekommt deshalb ein hoeheres Kontingent statt gar keines.

Die sieben Direkt-Mails entstehen einzeln, weil ein Mensch gerade geklickt hat
und wartet. Sie koennen keinen Schub bilden; Drosseln nuetzt dort nichts und
kostet einen Supportfall je verzoegertem Kennwort. Sie lassen sich deshalb auch
nicht nach unten verschieben.

Gebaut wird es als Zwischenschicht an der Mail selbst: alle vierzehn Klassen
sind bereits ShouldQueue, und kein einziger Aufrufer aendert sich. Verworfen:
eine eigene Ausgangstabelle (zwanzig Absendestellen umbauen, zwei
Warteschlangen nebeneinander) und drei Arbeiter mit festem Takt (die Zahlen
staenden im Container statt in der Konsole).

Die Falle steht im Entwurf, weil sie den Bau still kaputtmachen kann: der
Arbeiter laeuft mit --tries=3, und eine gedrosselte Rueckstellung zaehlt als
Versuch. Ohne Vorkehrung waere jede Rechnung nach dem dritten Drosseln
gescheitert statt verschickt. Das ist der erste Test, vor der ersten Zeile
Drossel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/versandtakt
nexxo 2026-08-03 15:18:47 +02:00
parent 412d67955a
commit 9e890243f6
1 changed files with 191 additions and 0 deletions

View File

@ -0,0 +1,191 @@
# Der Versandtakt
Entwurf vom 2. August 2026, beschlossen mit dem Betreiber. Erstes von fünf
Vorhaben rund um den Mailversand; die übrigen stehen am Ende unter „Was danach
kommt".
## Warum
Der Betreiber will nicht, dass ein großer Lauf — zweihundert Rechnungen an einem
Abend — als Schub bei einem einzelnen Empfängerserver ankommt und die
Absenderadresse dort als Spam eingestuft wird. Ein einmal beschädigter
Absenderruf ist schwer zu reparieren, und die Regel muss stehen, **bevor** der
erste große Lauf kommt.
Zur Einordnung, damit niemand den Aufwand für dringlicher hält als er ist: heute
sind es **11 Kunden, 5 laufende Verträge, 2 Rechnungen**. Bei dieser Größe
braucht es keine Drosselung. Gebaut wird sie für den Zustand, in dem sie fehlen
würde.
## Was heute steht
- **Vierzehn Mailklassen**, ausnahmslos `ShouldQueue` — es geht heute schon alles
über die Warteschlange (Redis).
- **Ein Arbeiter** für alles: `php artisan queue:work redis --tries=3 --timeout=90`.
- **Eine zweite Schlange existiert bereits**: die Bereitstellung fährt über
`onQueue('provisioning')` mit eigenem Arbeiter. Das Muster ist also im Haus.
- **`sent_mails`** protokolliert, was hinausging (Empfänger, Betreff, Mailklasse,
Zeitpunkt) — und heißt bewusst nicht „delivered": angenommen ist nicht
zugestellt.
- **Keine Drosselung, kein Takt, keine Priorität.**
## Die drei Spuren
Rund um die Uhr, ohne Zeitfenster. Der Betreiber hat das ausdrücklich so
entschieden: ob eine Rechnung um 18 Uhr oder um 2 Uhr ankommt, ändert nichts —
sie wird abgelegt, und die Abbuchung macht Stripe von selbst.
| Spur | Schlange | Takt | Mailklassen |
|---|---|---|---|
| **Direkt** | `mail-direkt` | ungedrosselt | ResetPassword, VerifyEmail, NewDeviceSignIn, SecurityBlock, ContactRequest, OrderConfirmation, OperatorMessage |
| **Wichtig** | `mail-wichtig` | 30 je 5 Minuten | MaintenanceAnnouncement, MaintenanceCancelled, CloudSuspended, CloudResumed |
| **Zeit lassen** | `mail-ruhig` | 20 je 10 Minuten | Invoice, DunningNotice, DormantAccountWarning |
**Warum „Direkt" ungedrosselt ist:** Diese Mails entstehen **einzeln**, weil ein
einzelner Mensch gerade etwas getan hat und auf die Antwort wartet. Sie können
gar keinen Schub bilden. Drosseln nützt dort nichts und kostet einen Supportfall
je verzögertem Kennwort.
`OperatorMessageMail` steht dort aus demselben Grund: der Betreiber hat gerade
auf „senden" geklickt und sieht zu. **Das ist meine Zuordnung, nicht seine**
sie gehört gegengelesen.
**Warum die Mahnung unten steht:** Sie hat eine Frist von Tagen, nicht von
Minuten, und geht an mehrere gleichzeitig. Bei 120 Mails je Stunde ist sie
spätestens nach einer Stunde draußen, lange vor jeder Frist.
**Warum eine Ausfallmeldung oben steht, obwohl sie an alle geht:** Massenversand
und Dringlichkeit sind zwei verschiedene Achsen. Eine Wartungsankündigung ist
beides — deshalb bekommt sie ein höheres Kontingent statt gar keines.
## Die Zuordnung ist änderbar, aber nicht überall
Die Vorgaben stehen im Code. Der Betreiber kann eine Mailklasse in der Konsole
verschieben; die Einstellung liegt darüber.
**Die sieben Direkt-Mails lassen sich nicht nach unten schieben.** Ein
Kennwort-Zurücksetzen, das zwanzig Minuten liegt, ist ein Supportfall und kein
gespartes Ansehen. Die Sperre sitzt in der Zuordnung selbst, nicht nur im
Formular.
**Eine neue Mailklasse ohne Eintrag landet in „Zeit lassen".** Das ist die
vorsichtige Richtung: eine zu langsam verschickte Mail ist ein kleinerer Fehler
als ein ungedrosselter Schub, den niemand vorhergesehen hat.
## Wie gedrosselt wird
**Eine Zwischenschicht an der Mail selbst.** Alle vierzehn Klassen sind bereits
`ShouldQueue`; sie bekommen eine gemeinsame Zwischenschicht, die vor dem Versand
das Kontingent ihrer Spur prüft. Ist es erschöpft, geht die Mail mit Verzögerung
zurück in die Schlange.
**Kein einziger Aufrufer ändert sich.** `Mail::to(...)->queue(...)` bleibt an
allen rund zwanzig Absendestellen stehen. Das ist der Grund für diesen Weg: der
Betreiber hat ausdrücklich verlangt, nichts kaputtzumachen, und dieser Weg fasst
die Absendestellen nicht an.
Verworfen wurden:
- **Eine eigene Ausgangstabelle**, aus der ein Befehl im Takt schöpft. Volle
Sicht und von Hand steuerbar — aber alle Absendestellen müssten umgebaut
werden, und es gäbe zwei Warteschlangen nebeneinander, die auseinanderlaufen
können.
- **Drei Arbeiter mit eigenem Takt** im Container. Am wenigsten Code — aber die
Zahlen stünden im Container statt in der Konsole, und niemand sähe, was wartet.
## Der Arbeiter muss die Spuren kennen
Drei Schlangen nützen nichts, wenn niemand sie liest. Der vorhandene Arbeiter
fährt heute `queue:work redis` ohne Angabe und bedient damit nur `default`.
Er bekommt die Spuren in dieser Reihenfolge:
```
queue:work redis --queue=mail-direkt,mail-wichtig,mail-ruhig,default --tries=3 --timeout=90
```
Die Reihenfolge **ist** die Priorität: der Arbeiter sieht erst nach, ob in
`mail-direkt` etwas liegt, und geht erst weiter, wenn dort nichts ist. Deshalb
steht ein Kennwort-Zurücksetzen nie hinter zweihundert Rechnungen — nicht wegen
einer Sortierung, sondern weil es in einer anderen Schlange liegt.
`default` bleibt am Ende stehen: dort läuft alles, was keine Mail ist, und es
darf durch diese Änderung nicht verhungern. Der eigene Arbeiter der
Bereitstellung (`provisioning`) bleibt unberührt.
**Das ist eine Änderung an `docker-compose.yml`** und damit am Deployment. Sie
muss zusammen mit dem Code ausgerollt werden: ein Arbeiter, der die neuen
Schlangen nicht liest, sammelt Mails an, die niemand versendet — still, ohne
Fehlermeldung.
## Die Falle, an der dieser Bau scheitern kann
Der Arbeiter läuft mit `--tries=3`. **Eine zurückgelegte Mail zählt als
Versuch.** Ohne Vorkehrung wäre jede Rechnung nach dem dritten Drosseln
endgültig gescheitert statt verschickt — still, im Fehlerprotokoll, ohne dass
jemand etwas merkt.
Eine gedrosselte Rückstellung darf den Versuchszähler deshalb **nicht** erhöhen.
Das ist der erste Test dieses Vorhabens, vor der ersten Zeile Drossel: fünfzig
Mails in eine Spur mit Kontingent zwanzig, und danach müssen fünfzig verschickt
sein und null gescheitert.
## Der Notschalter
Ein Schalter in der Konsole macht die Drossel wirkungslos — dann fährt alles wie
heute, ungedrosselt über die drei Schlangen. Wenn die Drossel je klemmt, muss der
Ausweg ein Klick sein und kein Deployment.
## Was sichtbar wird
Je Spur: wie viele Mails warten (Redis kennt die Länge), welcher Takt eingestellt
ist, und ob die Drossel an ist. Darunter das vorhandene Versandprotokoll.
Die Bereitschaftsseite schlägt an, wenn in einer Spur etwas liegt, das **älter
als eine Stunde** ist. Die Zahl ist so gewählt, dass sie beim langsamsten Takt
nicht von selbst anschlägt: 20 je 10 Minuten sind 120 in der Stunde, und ein Lauf
dieser Größe steht heute nicht an. Schlägt sie trotzdem an, klemmt etwas — und
das ist die Lage, die sonst niemand bemerkt.
## Tests
- **Der Versuchszähler.** Fünfzig Mails, Kontingent zwanzig: fünfzig verschickt,
null gescheitert. Der wichtigste Test dieses Vorhabens.
- **Die Spurzuordnung.** Jede der vierzehn Mailklassen landet in der Schlange
ihrer Vorgabe; eine unbekannte Klasse landet in „Zeit lassen".
- **Die Sperre.** Eine Direkt-Mail lässt sich nicht in eine gedrosselte Spur
verschieben — auch nicht an der Oberfläche vorbei.
- **Der Takt.** In einem Fenster gehen höchstens so viele Mails hinaus, wie das
Kontingent erlaubt; im nächsten wieder.
- **Direkt ist ungedrosselt.** Hundert Direkt-Mails hintereinander werden nicht
zurückgelegt.
- **Der Notschalter.** Ausgeschaltet fährt alles ohne Verzögerung.
- **Die Reihenfolge.** Eine Direkt-Mail, die nach zweihundert Rechnungen
eingereiht wird, geht vor ihnen hinaus.
## Was NICHT dazugehört
- **Ein Zeitfenster.** Ausdrücklich nicht gewollt: rund um die Uhr.
- **Umsortieren innerhalb einer Spur.** Die Trennung in drei Schlangen leistet,
was die Priorität leisten soll.
- **Der Zeitplan** (wann welche Mail ausgelöst wird), **der Vorlauf** (Rechnung
einige Tage vor der Abbuchung), **die Vorlagen für Systemmails samt Editor**
und **die Aufteilung der Mailseite**. Vier eigene Vorhaben.
## Was danach kommt
In dieser Reihenfolge, jedes mit eigenem Entwurf:
1. **Die Mailseite aufteilen** — Zustellung, Postfächer, Spuren, Protokoll.
2. **Vorlagen für Systemmails** mit Editor und Vorschau. Heikel: eine
Rechnungsmail trägt Pflichtangaben, frei bearbeitbar heißt auch frei
kaputtmachbar.
3. **Vorlauf** — die Rechnung einige Tage vor der Abbuchung statt danach. Braucht
Stripes Vorschau auf die nächste Abbuchung.
4. **Eigene Mails mit eigener Auslöseregel.** Die eigentliche Frage dort ist
nicht die Vorlage, sondern: wann wird sie verschickt.
Siehe `docs/superpowers/specs/2026-07-31-zahlungsmittel-und-mahnlauf-design.md`
für das Mahnwesen, das heute täglich um 09:00 läuft — mit derselben Begründung,
aus der die Spuren rund um die Uhr fahren dürfen und die Mahnung trotzdem unten
steht.