85.1.2. pepsi-dispatch

drive messages through the stage pipeline

Handbuchabschnitt:

1

85.1.2.1.1. Name

pepsi-dispatch - ausstehende Nachrichten beanspruchen und ihre Stage-Programme ausführen.

85.1.2.1.2. Übersicht

pepsi-dispatch [GLOBAL-OPTIONS] serve

85.1.2.1.3. Beschreibung

pepsi-dispatch ist der langlebige Dispatcher, der Nachrichten durch die Stage-Pipeline weiterschaltet (siehe pepsi.conf(5)). Er beansprucht pending-Datensätze aus pepsi.workqueue, setzt jeden auf running, schlägt die Stage der Nachricht in deren [stage-<stage>]-Abschnitt nach und übergibt die Nachrichten-ID an einen Worker-Prozess dieser Stage. Jede Stage läuft als Pool persistenter Worker, gestartet als PROGRAM worker (PROGRAM -c FILE worker, wenn CONFIG_FILE gesetzt ist — das globale Flag steht vor dem Unterbefehl, wie überall in Pepsi; das Programm wird auf dem PATH gesucht, sofern PROGRAM kein absoluter Pfad ist); ein Worker verbindet sich einmal mit der Datenbank und liest dann Nachrichten-IDs von seiner Standardeingabe und schreibt pro Nachricht eine Statuszeile auf seine Standardausgabe (0 bei Erfolg), in Eingabereihenfolge. Auf einen Status können ein Leerzeichen und ein JSON-Bericht über die Stages folgen, die in diesen Durchgang fusioniert wurden, [["<stage>",<microseconds>], …]; so bleiben die Statistiken pro Stage vollständig, ohne dass die Worker sie schreiben (siehe Statistiken unten). Nur ein unlesbarer Status ist fatal — er bringt das positionsbezogene Protokoll aus dem Takt, sodass der Dispatcher den Worker abbaut; ein unlesbarer Bericht wird mit einer Warnung verworfen, sodass ein Worker und ein Dispatcher aus verschiedenen Builds für die Dauer eines Neustartfensters Statistiken verlieren statt Nachrichten. Betreiben Sie genau einen Dispatcher pro System; er ist der einzige Prozess, der Stage-Programme startet (obwohl dann viele Worker gleichzeitig gegen die gemeinsame Datenbank laufen). Neue Arbeit wird über ein LISTEN auf dem workqueue-Kanal aufgenommen, mit einem periodischen Sicherheitsnetz-Heartbeat (POLL_INTERVAL).

Benachrichtigungen auf diesem Kanal werden ausdrücklich ausgelöst, und nur von Schreibern außerhalb der eigenen Schleife des Dispatchers: pepsi-ingress(1) (einmal je Schwall angenommener Nachrichten, nicht einmal je Nachricht), die SQL-Funktionen workqueue_inject und workqueue_resume, pepsi-keydisc(1), wenn es geparkte Mail freigibt, pepsi-failure-bouncer(1), die Reparaturbefehle von pepsi-queue(1) und die Moderations- und Sammelnachrichtenbefehle von pepsi-list(1). Eine Stage, die eine Nachricht weiterschaltet, benachrichtigt bewusst nicht: der Worker meldet die fertige Nachricht auf seiner Standardausgabe, und der Dispatcher antwortet darauf mit genau dem vollen Beanspruchen, das eine Benachrichtigung ausgelöst hätte, die Benachrichtigung trüge also keine Information. Ein Tabellen-Trigger, der bei jedem Datensatz benachrichtigt, der pending wird, wäre schlimmer als nutzlos: PostgreSQL hält von dem Moment an, in dem eine Transaktion eine Benachrichtigung einreiht, bis zu deren Commit eine datenbankweite Sperre, sodass jeder Stage-Hop die Commits der ganzen Datenbank serialisieren würde — siehe das Kapitel Performance des Handbuchs.

Gestapeltes Beanspruchen. Bei jedem Aufwachen (eine Benachrichtigung, ein freigewordener oder toter Worker, der Poll-Heartbeat, eine Listener-(Wieder-)Verbindung) leert der Dispatcher zunächst jedes ausstehende interne Ereignis, sodass seine Kapazitäten pro Stage aktuell sind, und führt dann ein UPDATE … RETURNING aus, das für jede Stage mit freier Kapazität auf einmal bis zu deren eigener verbleibender Kapazität beansprucht (pending``→``running; welche Datensätze, ist Gegenstand des nächsten Absatzes — bei ausgeschaltetem fairem Scheduling ist es eine LATERAL-Unterabfrage pro Stage, deren ORDER BY workqueue_id LIMIT cap den Scan des Rückstaus jeder Stage bei ihrer Obergrenze stoppt, angetrieben vom partiellen Index auf (stage, workqueue_id) WHERE status='pending'). Die Benachrichtigung trägt eine leere Nutzlast — eine Benachrichtigung ist einfach ein Aufwachen, und ein Schwall von ihnen kollabiert zu einem gestapelten Beanspruchen (PostgreSQL fasst auch identische eingereihte Benachrichtigungen zusammen).

Faires Scheduling zwischen Absendern. Bei Reihenfolge nach Alter könnte der Schwall eines Absenders jede dahinter eingereihte Nachricht verzögern, daher wählt das Beanspruchen standardmäßig (FAIR_SCHEDULING, siehe pepsi.conf(5)) aus, welche Datensätze die freie Kapazität einer Stage füllen, so wie der Linux-Scheduler (CFS/EEVDF) auswählt, welcher Prozess eine CPU bekommt: Eine Stage ist die CPU und ein Absender ist der Prozess. Der Absender eines Datensatzes ist die generierte Spalte workqueue.sender_key:

  • user:<login> — das authentifizierte Konto bei einer Submission (SASL oder die Peer-Credentials des lokalen Sockets), welchen Umschlagabsender es auch gewählt hat;

  • ip:<address> — andernfalls der verbindende SMTP-Peer, ein IPv6-Peer nach seinem /64 (einem einzelnen Host wird routinemäßig ein ganzes /64 zugeteilt). Bewusst nicht die Umschlag-Domain, die ein entfernter Absender je Nachricht frei wählt;

  • from:<domain> — für eine Nachricht, die Pepsi selbst erzeugt hat (keine SMTP-Herkunft: ein Hinweis, ein Bericht), die Domain des Umschlagabsenders;

  • null:<domain> — für eine mit Null-Absender (eine DSN, eine automatische Antwort) die Domain ihres ersten Empfängers.

Da der Schlüssel aus dem Datensatz generiert wird, erhält ihn jeder Schreiber — Ingress, eine injizierte Nachricht, eine Aufteilung, ein Klon —, ohne von seiner Existenz zu wissen. Der Dispatcher führt im Speicher und pro Stage den Vorsprung jedes Absenders: die Worker-Zeit, die dessen Nachrichten dort über den am wenigsten bedienten wartenden Absender hinaus verbraucht haben. Einer beanspruchten Nachricht wird sofort die durchschnittliche Zeit je Nachricht der Stage angerechnet, korrigiert auf die gemessene Zeit, wenn ihr Worker meldet, und erstattet, wenn sie neu eingereiht wird, ohne gelaufen zu sein; einer Nachricht mit Timeout wird die ganze MAX_RUNTIME angerechnet. Das Beanspruchen ordnet dann den k-ältesten wartenden Datensatz eines Absenders mit Vorsprung L nach L + k × average, den kleinsten zuerst, sodass ein Absender mit tausend wartenden Datensätzen sich mit einem abwechselt, der einen einzigen Datensatz hat, statt zuerst dranzukommen — und da die Anrechnung Zeit statt einer Anzahl ist, erhält ein Absender, dessen Nachrichten an einer Stage teuer sind, entsprechend weniger ihrer Slots. Ein Absender, der neu an der Stage ist oder dessen Vorsprung abgeklungen ist, beginnt gleichauf mit dem am wenigsten bedienten, sodass Stillhalten keine spätere Priorität erkauft. Vorsprünge halbieren sich alle FAIR_HALF_LIFE (Standardwert 60 s), und ein Vorsprung, der weniger als eine Nachricht wert ist, wird vergessen; ein Neustart vergisst sie alle, was harmlos ist. Um zu garantieren, dass kein Datensatz für immer wartet — wie viele neue, gleichauf liegende Absender auch immer eintreffen —, gehen FAIR_FIFO_PERCENT (Standardwert 10) der beanspruchten Slots jeder Stage unabhängig vom Absender an ihre ältesten wartenden Datensätze, sodass eine Nachricht höchstens so lange wartet, bis die vor ihr an dieser Stage eingereihten Datensätze mit dieser reservierten Rate abgearbeitet sind.

Das Beanspruchen ist weiterhin eine einzige Anweisung für alle Stages auf einmal, der pending``→``running-Schreibvorgang des einzigen Beanspruchers, pro Stage durch deren Kapazität begrenzt: Die SQL-Funktion workqueue_claim_fair erhält die Vorsprünge als Arrays, zählt die an jeder Stage wartenden Absender mit einem Loose Index Scan auf dem partiellen Index (stage, sender_key, workqueue_id) WHERE status='pending' auf (eine Abfrage je Absender, höchstens 64 Absender je Stage und Beanspruchen — darüber hinaus rotiert das Fenster von einem Beanspruchen zum nächsten) und liest höchstens die Kapazität der Stage an ältesten Datensätzen jedes Absenders, sodass ihre Kosten nicht mit der Tiefe des Rückstaus eines einzelnen Absenders wachsen. FAIR_SCHEDULING = no stellt das einfache Beanspruchen nach Alter wieder her.

Elastische Worker-Pools mit Pipelining. Der Pool jeder Stage wächst bei Bedarf bis zu seinem PARALLELISM (Standardwert 4) und schrumpft im Leerlauf: Kein Worker läuft, bis die Stage Verkehr sieht; hat eine Stage Arbeit und freie Kapazität, füllt der Dispatcher den freien Slot eines bestehenden Workers oder startet einen neuen Worker; Arbeit wird auf möglichst wenige Worker gepackt, sodass ein Überschuss kalt wird und nach WORKER_IDLE_TIMEOUT (Standardwert 5 s) gestoppt wird. Bei stetiger leichter Last pendelt eine Stage daher um einen einzelnen Worker. Einem Worker werden bis zu QUEUE_LIMIT (Standardwert 4) Nachrichten-IDs auf einmal übergeben — in einem einzigen Puffer auf seine Standardeingabe geschrieben — statt einzeln; er verarbeitet sie weiterhin streng seriell, aber die nächste ID ist bereits eingereiht, sodass der Abschluss einer Nachricht ihn ohne vorherigen Koordinator-Round-Trip freigibt. Die gesamte in Bearbeitung befindliche Kapazität einer Stage ist somit QUEUE_LIMIT × PARALLELISM. Weil die Worker-Anzahl (und damit die Datenbankverbindungszahl) unverändert ist, tauscht das Erhöhen von QUEUE_LIMIT ein wenig Head-of-Line-Latenz gegen Durchsatz, ohne mehr Verbindungen zu verbrauchen. Nach MAX_MESSAGES (Standardwert 1000) erneuert ein Worker seinen Kindprozess — sobald seine Pipeline geleert ist — mit einem frischen Prozess, um das Speicherwachstum zu begrenzen.

Gestapelte Stages ohne Nachrichtentext. Ein Worker für eine Stage, die weder den Header-Block noch den Nachrichtentext lädt (die Nur-Metadaten-Stages — srs, if, auto-whitelist, discard, aliases …), verarbeitet die ihm per Pipelining übergebenen IDs in einem Stapel statt einzeln: Er nimmt gierig jede bereits auf seiner Standardeingabe gepufferte ID (bis zu QUEUE_LIMIT, und stoppt in dem Moment, in dem ein Lesevorgang blockieren würde, sodass eine einzelne ID weiterhin sofort behandelt wird), lädt sie alle in einem einzigen SELECT … WHERE workqueue_id = ANY(...), führt jeden Stage-Rumpf aus und committet dann die Weiterschaltungen/Fehlschläge/Wiedereinreihungen in einem über Arrays laufenden UPDATE … FROM jsonb_to_recordset(...) je Ergebnisform (typischerweise eine). Er schreibt weiterhin je ID eine Statuszeile, der Reihe nach, sodass das Worker-Protokoll unverändert ist. Wenn die Warteschlange voll ist, senkt dies die Datenbank-Round-Trips einer Stage ohne Nachrichtentext von zwei je Nachricht auf etwa zwei je QUEUE_LIMIT Nachrichten. Stages, die die Header oder den Nachrichtentext laden, behalten den Pfad mit einer ID auf einmal (ihre großen Spalten lassen sich nicht günstig in Arrays fassen).

Weiterschalten. Eine Stage schaltet eine Nachricht weiter, indem sie ihre stage-Spalte auf die nächste Stage und den Datensatz zurück auf pending in einem Update setzt; der Dispatcher schreibt einen Datensatz bei Erfolg nie um. Das Beanspruchen (pending``→``running) ist der einzige Schreibvorgang des Dispatchers auf den status eines Datensatzes für Vorwärtsfortschritt, und er ist der einzige Schreiber, der ihn ausführt. Wenn ein Worker Erfolg meldet (Status 0), sucht der Dispatcher erneut nach Arbeit — genau deshalb löst ein Weiterschalten keine eigene Benachrichtigung aus —, sodass der Pool der nächsten Stage den nun pending-Datensatz in der folgenden Einplanungsrunde beansprucht. Die Nachricht ist fertig, wenn der Worker den Datensatz löscht, pausiert, wenn er ihn paused belässt, oder terminal, wenn failed/timeout. Eine Stage committet ihr Ergebnis in einem einzigen Round-Trip — ein UPDATE/DELETE oder eine gespeicherte Funktion, die Arbeit serverseitig auffächert (ein Empfänger-Split oder ein Finish/Pause, das auch Bounce-, Delay- oder Success-DSNs aus Arrays von Klonen erzeugt) — niemals eine mehranweisige Client-Transaktion. Die Verbindung eines Workers läuft zudem mit PostgreSQLs synchronous_commit aus ([pepsi-postgres] WORKER_SYNCHRONOUS_COMMIT, Standardwert no), was gleichzeitigen Workern das Gruppen-Commit erlaubt. Aufgegeben wird damit nichts als die Dauerhaftigkeit der letzten paar hundert Millisekunden, und ein so verlorenes Weiterschalten einer Stage ist nicht davon zu unterscheiden, dass ein Worker unmittelbar davor abgeschossen wurde: Der Datensatz wird bei der vorherigen Stage als running vorgefunden, beim nächsten Start auf pending zurückgesetzt, und die Stage läuft erneut. pepsi-ingress(1) behält bewusst den Standardwert bei, denn eine Annahme zu verlieren, die es bereits mit 250 beantwortet hat, wäre eine verlorene Nachricht statt einer wiederholten Stage.

Fehler und Wiederholungen. Ein Stage-Worker meldet sich mit einer ready-Zeile, sobald er seinen Datenbank-Pool geöffnet und seine Konfiguration geprüft hat, und beantwortet danach jede Nachricht mit einer Statuszeile. Ein Worker, der einen Status ungleich null meldet, belässt diese Nachricht als failed mit dem Grund in state und bleibt für die nächste ID am Leben; ein Worker meldet das aber nur für einen Fehler, den seine Stage als permanent markiert hat (siehe Stage-Fehler weiter unten), oder nachdem er die Wiederholungen aufgegeben hat, und nie für die beiden Datenbank-Status. Ein Worker, dessen Kindprozess seine Ausgabe schließt (ein Absturz), nachdem er bereit war, wird abgebaut, und seine vorderste Nachricht erhält einen Strike; ebenso die vorderste Nachricht eines Kindprozesses, der nicht innerhalb von MAX_RUNTIME (Echtzeit) antwortet und deshalb beendet wird. Eine Nachricht mit einem Strike wird pausiert und nach einer Minute erneut versucht (nach dem zweiten Strike nach zwei), mit dem Grund in state.last_error und der Anzahl in state.strikes; erst ihr dritter Strike gilt als von der Nachricht selbst verursacht und setzt sie auf failed (nach einem Absturz) oder timeout (nach einem Hänger), mit state.failure_class crashed bzw. timed-out. Ein Worker stirbt auch aus eigenen Gründen – der OOM-Killer, ein Helfer, der gerade aktualisiert wird, ein Resolver, der hängt –, und ein solcher Tod darf nicht die Nachricht kosten, die er gerade hielt, während eine Nachricht, die jeden ihr übergebenen Worker umbringt, nicht ewig wiederholt werden darf. In beiden Fällen liefen die anderen bereits an diesen Worker pipelinten Nachrichten nie, daher werden sie auf pending zurückgesetzt und erneut dispatcht (eine einzelne festhängende oder vergiftete Nachricht strandet somit ihre in Bearbeitung befindlichen Geschwister nicht). Ein Kindprozess, der stirbt, bevor er bereit war, gibt niemandem die Schuld: Er ist an seinem eigenen Start gescheitert – ein nicht erreichbares oder verbindungserschöpftes PostgreSQL, ein unlesbares Geheimnis –, daher geht jede ihm übergebene Nachricht zurück auf pending, und die Stage wird für eine kurze Abkühlzeit zurückgehalten, bevor ein weiterer Worker gestartet wird, statt ihn sofort neu zu starten. Ein Kindprozess, der vor jeder Antwort mit Status 78 (EX_CONFIG) endet, hat sich geweigert zu laufen – weil er auf ein Datenbankschema aus einem anderen Release traf, im Zeitfenster einer Paketaktualisierung zwischen dem Eintreffen der neuen Binaries und dem Upgrade des Schemas, oder weil sich seine Konfiguration nicht parsen lässt –, und wird ebenso behandelt, mit einer Fehlermeldung, die die Ursache nennt; die Stage wird nach der Abkühlzeit erneut versucht, bis das Schema passt oder die Konfiguration korrigiert ist. Die MAX_RUNTIME-Uhr läuft je Nachricht und beginnt, wenn eine Nachricht an die Spitze der Worker-Warteschlange gelangt, sodass einer Nachricht, die hinter einem langsamen Geschwister wartet, das Warten nicht angerechnet wird. Wenn eine Stage eine Nachricht paused belässt, hält sie in timeout fest, wann die Nachricht wiederholt werden soll; der Dispatcher schläft bis zur frühesten solchen Zeit, kippt die fälligen Datensätze zurück auf pending und scannt erneut nach Arbeit. Beim Start setzt er etwaige übriggebliebene running-Datensätze (von einem vorherigen Dispatcher verwaist) zurück auf pending. Weil es genau einen Dispatcher gibt und sein Koordinator seriell beansprucht — der einzige Schreiber, der pending``→``running setzt —, benötigt das gestapelte Beanspruchen je Stage kein FOR UPDATE SKIP LOCKED. „Genau einer“ wird erzwungen und nicht vorausgesetzt: beim Start nimmt der Dispatcher eine sitzungsweite Advisory-Sperre von PostgreSQL auf seine Datenbank und weigert sich zu laufen, wenn ein anderer Dispatcher sie nach etwa zehn Sekunden noch immer hält. Ohne das würde der eigene Startdurchlauf eines zweiten Dispatchers sofort die in Bearbeitung befindlichen Datensätze des ersten freigeben und beide würden sie versenden — doppelte Zustellung, doppelte Bounces, doppelter Zahlungsausgleich. Die Sperre wird von PostgreSQL freigegeben, wenn die Sitzung endet, ein abgeschossener Dispatcher hinterlässt also nichts aufzuräumen. Weil höchstens ein Schreiber jemals einen gegebenen Datensatz berührt, läuft die gemeinsame Datenbankverbindung mit READ COMMITTED statt SERIALIZABLE (Warteschlangenoperationen werden dennoch durch einen Wiederholungs-Helper als günstige Versicherung geleitet).

Telemetrie-Schalter. Der Dispatcher lauscht außerdem auf telemetry_changed, das jeder sendet, der [pepsi] SHARE_TELEMETRY in der Konfigurationsdatei umschreibt, nachdem er dies getan hat, und setzt jeden laufenden Worker außer Dienst: Ein Worker richtet seine Feature-Telemetrie-Senke einmal ein, aus der Datei, sodass sein Nachfolger entsprechend mit dem Melden beginnt (oder aufhört). Die Stage-Tabelle bleibt unberührt. Siehe pepsi-telemetry-client(1).

Neuladen der Konfiguration. Jeder [stage-*]-Abschnitt ist eine heiße Einstellung: Auf die Benachrichtigung config_changed hin baut der Dispatcher seine eigene Stage-Tabelle aus dem Konfigurations-Overlay neu auf und setzt jeden laufenden Worker außer Dienst, sodass eine Stage hinzugefügt, bearbeitet oder entfernt werden kann, während die Pipeline läuft, und die nächste Nachricht sowohl mit den neuen Werten geroutet als auch ausgeführt wird. Ein Stage-Graph, der sich nicht parsen lässt, beendet den Prozess mit Exit-Code 78 nach einem geordneten Herunterfahren (Worker gestoppt, ihre Datensätze zurück auf pending), und der Parse-Fehler wird geloggt. Die vorherige Tabelle zu behalten wäre schlimmer: Die Worker werden durch dasselbe Ereignis außer Dienst gesetzt, und ihre Nachfolger lesen das neue Overlay, sodass der Dispatcher nach einer Pipeline routen würde, während die Stages eine andere ausführten. Die mitgelieferte pepsi-dispatch.service verwendet Restart=always ohne Startlimit: Ohne den Dispatcher sammelt sich angenommene Mail nur in der Warteschlange an, daher versucht es die Unit immer wieder, mit einem Backoff von 2 s bis auf einmal pro Minute (RestartSteps=, RestartMaxDelaySec=), und übernimmt eine korrigierte Konfiguration von selbst. Ein Overlay, das sich nicht lesen lässt (ein Datenbankfehler), ist keine neue Konfiguration: Der Dispatcher behält seinen aktuellen Stage-Graphen und versucht das Neuladen erneut, statt jede nur in der Datenbank definierte Stage fallen zu lassen. Der Abschnitt [pepsi-dispatch] selbst wird nicht neu geladen — der Verbindungspool muss existieren, bevor das Overlay gelesen werden kann —, eine Änderung daran braucht also einen Neustart.

Datenbank-Überlast-Gegendruck. Ein Worker, der die Datenbank nicht erreichen kann, weil sein Verbindungslimit erschöpft ist (PostgreSQL too_many_connections, oder sein Pool läuft beim Beschaffen einer Verbindung in einen Timeout), meldet einen eigenen Status (EX_TEMPFAIL, 75) statt des generischen Fehlercodes — dies ist Infrastrukturdruck, kein Defekt in der Nachricht. Der Dispatcher lässt die Nachricht dann nicht fehlschlagen: Er reiht sie für eine spätere Wiederholung erneut als pending ein, und um Verbindungen abzubauen, reduziert er vorübergehend die Parallelität der Stage, die derzeit die meisten Worker-Prozesse betreibt (der größte Verursacher des Drucks, der nicht die Stage sein muss, die den Fehler meldete) — und halbiert ihre Obergrenze, Untergrenze 1, für fünf Minuten. Wiederholte Meldungen drosseln diese Stage weiter herunter und frischen das Fenster auf; bestehende Worker leeren sich über den Leerlauf-Abräumer, und die Stage fährt wieder hoch, sobald das Fenster verstreicht. Das Mittel gegen anhaltenden Druck ist, das max_connections von PostgreSQL zu erhöhen oder das PARALLELISM der Stages zu senken, sodass die Summe der Pools jeder Komponente auf den Server passt (siehe den [pepsi-postgres]-Verbindungsbudget-Hinweis in pepsi.conf(5)).

Verlorene Datenbankverbindung. Ein Worker, dessen Datenbankverbindung abbricht oder nicht hergestellt werden kann, während er eine Nachricht bearbeitet (PostgreSQL startet neu oder fährt herunter, eine Netzwerkunterbrechung: ein E/A- oder TLS-Fehler, SQLSTATE-Klasse 08, 57P01–57P03), meldet Status 74 (EX_IOERR). Auch das ist kein Defekt in der Nachricht, daher lässt der Dispatcher sie nicht fehlschlagen: Er setzt sie für 30 Sekunden auf paused, danach übergibt sie der gewöhnliche Durchlauf für pausierte Wiederholungen wieder derselben Stage. Das wiederholt sich, solange die Datenbank nicht erreichbar bleibt. Nichts, was die Stage getan hat, wurde committet, daher führt die Wiederholung die Stage von vorn aus; ein Seiteneffekt außerhalb der Datenbank, dessen Commit verloren ging (ein Relay, das die Nachricht kurz vor dem Abbruch der Verbindung zugestellt hat), kann ein zweites Mal eintreten. Jeder Aufschub wird als Warnung unter pepsi-dispatch protokolliert und nirgends sonst gezählt: Ein Zähler müsste in die Datenbank geschrieben werden, die gerade weggefallen ist. Kann der Dispatcher auch die Pause nicht schreiben, versucht er es etwa eine halbe Minute lang erneut und belässt die Nachricht andernfalls in running, was sein nächster Start auf pending zurücksetzt.

Stage-Fehler. Ein Fehler, den eine Stage zurückgibt, gilt als Fehler des Hosts, sofern die Stage ihn nicht als permanent markiert – eine Eingabe, die sie nicht parsen kann, ein Konstrukt, das sie ablehnt. Ein Host-Fehler ist ein Helfer, der nicht gestartet werden kann, eine Vorlage, Map, ein Schlüssel oder Trust-Store, der nicht gelesen werden kann, eine Datenbankanweisung, die aus einem anderen Grund als der Verbindung abgelehnt wird, ein Dienst, der nicht antwortet. Der Worker meldet ihn nicht als Fehlschlag. Er pausiert die Nachricht in der Stage, in der sie beansprucht wurde, hält den Fehler als state.last_error und die Anzahl solcher Wiederholungen in dieser Stage als state.temporary_failures fest und meldet Erfolg; der Durchlauf für pausierte Wiederholungen gibt die Nachricht nach einer Minute zurück, dann nach zwei, vier und so weiter bis zu einer Stunde. Jede Wiederholung wird als Warnung unter dem Namen der Stage protokolliert. Sobald die Nachricht länger als die MAX_LIFETIME der Stage wiederholt wurde (Standard 120 Stunden, siehe pepsi.conf(5); gezählt ab der Ankunft oder, bei einer DSN, ab dem Zeitpunkt, an dem pepsi-stage-bounce(1) sie erzeugt hat), gibt der Worker auf: Er leitet die Nachricht an die BOUNCE_STAGE der Stage um, die sie dem Absender meldet, oder – ohne BOUNCE_STAGE oder bei einer Mailinglisten-Kopie – lässt sie fehlschlagen, und pepsi-failure-bouncer(1) übernimmt. Ein permanenter Fehler lässt die Nachricht sofort fehlschlagen. In beiden Fällen sagen state.last_error, state.failed_stage und state.failure_class (permanent oder retries-exhausted), was geschehen ist; die DSN selbst trägt nur einen allgemeinen Grund, weil der festgehaltene Fehler Dateien, Datenbankobjekte und Hosts nennen kann.

Der Standard ist so herum, weil die beiden Fehler nicht gleichwertig sind: Ein Host-Fehler, der als Defekt der Nachricht behandelt wird, teilt einem Absender mit, seine Mail sei unzustellbar, weil der Server zehn Minuten lang falsch konfiguriert war, und lässt sich nicht zurücknehmen, während ein Defekt, der als Host-Fehler behandelt wird, Wiederholungen und einen verspäteten Bericht kostet. Die eigene Konfiguration einer Stage wird beim Start ihres Workers geprüft, nicht pro Nachricht: Ein Abschnitt oder Geheimnis, das sich nicht parsen lässt, lässt den Worker den Start verweigern (Status 78, siehe oben), sodass die Warteschlange auf die Korrektur wartet, statt jede Nachricht einzeln fehlschlagen zu lassen. Eine adressbezogene Überschreibung, die den Abschnitt einer Stage unparsbar macht, ist der einzige Konfigurationsfehler, der permanent ist, weil er dem Korrespondenten gehört und nicht dem Host.

Schlägt eine Stage fehl, die in den Durchlauf einer anderen Stage fusioniert wurde, wird die Kette bis zu ihr zuerst committet – der Datensatz wird in die fehlschlagende Stage verschoben, weiterhin running –, und der Fehler wird dort behandelt, mit der MAX_LIFETIME und BOUNCE_STAGE dieser Stage; eine Wiederholung setzt dann bei dieser Stage fort, statt die Stages davor zu wiederholen.

Einstellungen pro Adresse. Bevor ein Worker die Logik einer Stage ausführt, konsultiert er die pepsi.settings-Tabelle (siehe pepsi-settings(1)) für den Korrespondenten der Nachricht — den Umschlagabsender einer state.local_origin-Nachricht, sonst ihre Empfänger — und legt etwaige Überschreibungen pro Adresse über die [stage-<name>]-Optionen der Stage. Die Überschreibungen werden mit dem Nachrichten-Datensatz in derselben Abfrage geholt (eine korrelierte Unterabfrage auf die relevanten Adressen), sodass das Laden einer Nachricht und ihrer Einstellungen ein einziger Round-Trip ist. Wenn die Empfänger einer eingehenden Nachricht auf unterschiedliche Überschreibungen für die gleich laufende Stage auflösen, teilt der Worker die Nachricht lazy auf: In einem Round-Trip (die workqueue_split-Funktion) gruppiert er die Empfänger nach ihrer effektiven Überschreibung, behält die erste Gruppe auf dem aktuellen Datensatz und fächert jede verbleibende Gruppe als neuen pending-Datensatz an derselben Stage auf (für sie wird keine Benachrichtigung ausgelöst: der eigene Abschluss des Workers bringt den Koordinator wie oben zu demselben Beanspruchen). Jeder Datensatz führt die Stage dann unter seinen eigenen Einstellungen aus. Eine Nachricht, deren Empfänger nie divergieren, wird nie aufgeteilt.

Stage-Fusion. Viele Stages sind sehr schnell und benötigen nur die Nachrichten-Metadaten (den Umschlag, das extrahierte From:/Subject: und das state-JSON), nicht den Header-Block oder den Nachrichtentext. Wenn eine solche Stage zu einem Nachfolger weiterschaltet, der nicht mehr Daten benötigt, als bereits geladen sind, ist es reiner Mehraufwand, den Datensatz zurück durch die Datenbank und den Dispatcher zu leiten. Stage-Fusion entfernt ihn: Statt den Datensatz pending an der nächsten Stage zu schreiben und zu warten, bis er beansprucht wird, führt der Worker den Rumpf des Nachfolgers im selben Prozess aus und verwendet den bereits geladenen Datensatz wieder. Eine ganze Kette fusionierter Stages ist ein SELECT am Anfang, die eigene Arbeit der Stages und ein einziger terminaler Schreibvorgang, dem Dispatcher als ein Abschluss gemeldet.

Ein Nachfolger wird nur fusioniert, wenn alles zutrifft: Stage-Fusion ist global aktiviert ([pepsi] ALLOW_FUSION, standardmäßig an); der Abschnitt des Nachfolgers ist mit FUSION = yes markiert (der Standardwert für die schnellen Stages ohne Nachrichtentext — pepsi-stage-if(1), pepsi-stage-discard(1), pepsi-stage-srs(1), pepsi-stage-check-whitelist(1), pepsi-stage-auto-whitelist(1), pepsi-stage-block-language(1), pepsi-stage-list(1), pepsi-stage-edit-settings(1)); das PROGRAM des Nachfolgers ist in dasselbe vereinheitlichte pepsi-Binärprogramm eingefaltet (sodass es im Prozess laufen kann — Fusion ist daher in einem programmweisen multibin-Build inaktiv); und der Nachfolger benötigt keine Nachrichtenspalte, die der Vorgänger nicht geladen hat. Jeder Fehlschlag macht das Weiterschalten zu einem gewöhnlichen, vom Dispatcher vermittelten Hop, sodass Fusion das Ergebnis einer Nachricht nie ändert — nur, ob die Übergabe die Datenbank berührt. Eine Stage kann zusätzlich eine datenabhängige Schranke anwenden, die Fusion für manche Nachrichten verweigert: pepsi-stage-edit-settings(1) lädt den vollständigen Nachrichtentext, tut aber nichts an einer Nachricht, die keine Einstellungs-Steuernachricht ist, sodass es jede Nicht-Steuernachricht allein anhand der Metadaten durchfusioniert und Fusion nur für eine echte Steuernachricht ablehnt — die dann gewöhnlich committet und vermittelt wird, sodass der Worker ihren Nachrichtentext lädt. Einstellungen je Adresse werden weiterhin auf jede fusionierte Stage angewendet (ihre Überschreibungen wurden bereits mit dem Datensatz geholt), und ein divergierender Empfänger-Split geschieht weiterhin, wo nötig — ein Hop, bei dem beides zutreffen könnte, wird aber nicht fusioniert: hat eine Stage der Kette die Nachricht umgeschrieben (Umschlagabsender, From:, Subject:, Header-Block oder Nachrichtentext) und trägt der Datensatz noch mehr als einen Empfänger, wird das Weiterschalten gewöhnlich committet, sodass ein anschließender Split je Adresse die Geschwister aus einem Datensatz klont, der die Umschreibung bereits trägt. Ein fehlkonfigurierter Stage-Zyklus wird durch eine Fusionstiefen-Obergrenze begrenzt, nach der das Weiterschalten gewöhnlich committet wird. Fusionierte Stages werden in pepsi.stage_stats weiterhin einzeln gezählt, sodass die Statistiken je Stage genau bleiben — der Worker meldet sie auf der Statuszeile des Durchgangs und der Dispatcher rechnet sie ein (siehe Statistiken); nur der Kosten-Benchmark je Stage schaltet die Fusion aus (mit ALLOW_FUSION = no), damit er jede Stage als ihren eigenen vom Dispatcher vermittelten Worker messen kann.

Statistiken. Die kumulativen Zähler in pepsi.stage_stats und pepsi.dispatch_stats sammelt der Dispatcher im Speicher an und schreibt sie in einer einzigen Transaktion: bei jedem STATS_INTERVAL, immer wenn die Pipeline in den Leerlauf geht (sodass die Zahlen eines Schwalls sichtbar sind, sobald er endet, statt bis zu ein Intervall später — begrenzt auf höchstens ein solches Schreiben je Sekunde) und beim Herunterfahren. Ein leerlaufender Dispatcher verrichtet überhaupt keine Datenbankarbeit, denn ein Schreiben ohne angesammelte Deltas schreibt nichts.

Nachrichtentexte. Derselbe STATS_INTERVAL-Takt durchsucht pepsi.workqueue_body nach Nachrichtentexten, auf die kein eingereihter Datensatz mehr verweist (pepsi.workqueue_body_gc(), höchstens 10 000 je Durchlauf). Ein Nachrichtentext wird von jedem Datensatz geteilt, der von einer Nachricht abgespalten wurde, und wird normalerweise von einem Trigger in dem Moment gelöscht, in dem der letzte von ihnen verschwindet; der Durchlauf sammelt den seltenen Nachrichtentext ein, den zwei gleichzeitige Löschvorgänge jeweils dem anderen überlassen haben. Verwaiste Texte kosten bis dahin nur Plattenplatz, nie eine Nachricht.

Der Dispatcher ist bewusst der einzige Schreiber dieser Tabellen, weshalb ihm ein fusionierter Hop auf der Statuszeile des Workers gemeldet und nicht vom Worker geschrieben wird: stage_stats hält einen Datensatz je Stage, ein je Nachricht geschriebener Zähler würde also jeden Worker einer Stage auf diesen einen Datensatz serialisieren. Die Zahlen auf einer Zeile mitzuführen, die der Worker ohnehin schreibt, beseitigt den Schreibvorgang ganz. Der Tausch ist der für Statistiken übliche: noch nicht geschriebene Deltas gehen verloren, wenn der Dispatcher stirbt, weshalb diese Zähler als bestmöglich dokumentiert sind. stages_executed zählt Durchgänge eines Workers, eine fusionierte Kette zählt also einmal, wie viele Stages sie auch umfasst.

pepsi-dispatch verarbeitet Nachrichten nicht selbst — es betreibt nur die Stage-Worker, die ihr eigenes Ergebnis auf dem Datensatz festhalten.

85.1.2.1.4. Befehle

serve

Führt den Dispatcher aus, bis er unterbrochen wird. Erfordert, dass das Schema mit pepsi-setup(1) installiert wurde.

85.1.2.1.5. Globale Optionen

Diese globalen Optionen stehen vor dem Unterbefehl (ein nachgestelltes Flag wird abgelehnt).

-c FILE, –config FILE

Liest die Konfiguration aus FILE statt die Standardorte zu durchsuchen. Setzen Sie CONFIG_FILE in [pepsi-dispatch] auf denselben Pfad, damit gestartete Stage-Programme ihn erben (siehe pepsi.conf(5)).

-L LOGLEVEL, –log LOGLEVEL

Setzt die Log-Ausführlichkeit (Standardwert info).

-v, –verbose

Zeigt Log-Meldungen aus allen Quellen.

-h, –help; -V, –version

Gibt eine Verwendungsübersicht / die Version aus und beendet sich.

85.1.2.1.6. Signale

SIGINT, SIGTERM

Leitet das Herunterfahren ein: hört auf, neue Arbeit zu beanspruchen, stoppt jeden Worker (und beendet seinen Kindprozess), setzt jeden in Bearbeitung befindlichen oder beanspruchten, aber nicht zugewiesenen Datensatz zurück auf pending und beendet sich.

85.1.2.1.7. Exit-Status

0

Sauberes Herunterfahren.

1

Ein Fehler ist aufgetreten (zum Beispiel eine fehlerhafte Konfigurationsdatei oder eine fehlgeschlagene Datenbankverbindung). Der Grund wird in das Journal geschrieben.

78

EX_CONFIG: ein Neuladen der Konfiguration ergab einen Stage-Graphen, der sich nicht parsen lässt, daher hat sich der Dispatcher heruntergefahren, statt nach einer Pipeline zu routen, die seine Stage-Worker nicht mehr teilen. Reparieren Sie die [stage-*]-Abschnitte (in der Datei oder in der Überlagerung pepsi.config_override) und starten Sie die Unit erneut. Wird auch beim Start zurückgegeben, wenn das Datenbankschema nicht dasjenige ist, mit dem dieses Release gebaut wurde (älter, neuer oder aus anderen Dateien gebaut); die Meldung sagt, welcher Fall vorliegt, und pepsi-setup schema aktualisiert ein älteres (siehe pepsi-setup(1)).

85.1.2.1.8. Beispiele

Den Dispatcher ausführen:

pepsi-dispatch -c /etc/pepsi/pepsi.conf serve

85.1.2.1.9. Siehe auch

pepsi-config(1), pepsi.conf(5), pepsi-ingress(1), pepsi-stage-srs(1), pepsi-stage-bounce(1), pepsi-stage-dkim-sign(1), pepsi-stage-relay-to-smarthost(1), pepsi-stage-relay-to-internet(1), pepsi-settings(1), pepsi-failure-bouncer(1), pepsi-queue(1), pepsi-setup(1)

85.1.2.1.10. Fehler

Melden Sie Fehler an den Pepsi-Issue-Tracker.