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
parent
412d67955a
commit
9e890243f6
|
|
@ -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.
|
||||
Loading…
Reference in New Issue