Terminplaner: veraltete G01 verworfen, Index bookingId
Eine G01, die erst nach holdMinutes + 1 Minute faellig wird, ist mit verfallenem Link nutzlos: sie wird nicht mehr gesendet, sondern verworfen (kein Transportaufruf, kein Budgetplatz, Felder geleert, kein Zustellfehler, raeumeAuf loescht sie nach 48 Stunden). Neuer Index idx_booking auf termineOutbox fuer zustellFehler und verwerfeWartende; verworfeneG01() zaehlt seit Prozessstart. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
36b94c3035
commit
43b3caddde
4 files changed
+268
-18
No files matched your search
@@ -19,6 +19,13 @@ import { pruefeNow, pruefeText, zeitMs } from './util.js'
|
||||
* Fehlschlag `fehler`
|
||||
* Nach `gesendet` werden an, html, text und ics geleert (personenbezogene Daten).
|
||||
*
|
||||
* Veraltete Bestaetigungsmail (G01 mit Link): Wird eine G01 erst nach Ablauf des Halte-Fensters faellig (Warteschlange voll,
|
||||
* Pause, Wiederholungen), waere ihr Bestaetigungslink schon verfallen. Sie geht dann nicht mehr raus, sondern wird `verworfen`
|
||||
* (siehe sende): kein Transportaufruf, kein Budgetplatz, an/html/text/ics geleert. Das ist kein Zustellfehler (zustellFehler
|
||||
* zaehlt nur `fehler`); raeumeAuf loescht `verworfen` wie `fehler` 48 Stunden spaeter. G01 ohne Link (bookingId '-', zu_viele)
|
||||
* und alle anderen Arten verfallen nie. Ein Eintrag, der schon rausging, aber nicht verbucht werden konnte (`unverbucht`),
|
||||
* wird nie verworfen und nie doppelt gesendet.
|
||||
*
|
||||
* `naechsterVersuch` ist die Faelligkeit des Eintrags: bei `wartet` der naechste Sendeversuch, bei `gesendet` der
|
||||
* Loeschtermin (Versand + 7 Tage), bei `fehler` ebenfalls (Fehlschlag + 48 Stunden). So findet raeumeAuf alles ueber
|
||||
* den vorhandenen Index idx_status_next (status, naechsterVersuch); ein eigenes Fehlerdatum gibt es im Schema nicht.
|
||||
@@ -52,6 +59,10 @@ const WIEDERHOLUNG_MS = [1 * MIN_MS, 5 * MIN_MS, 30 * MIN_MS]
|
||||
const ANMELDE_PAUSE_MS = 15 * MIN_MS
|
||||
const GESENDET_BEHALTEN_MS = 7 * TAG_MS
|
||||
const FEHLER_BEHALTEN_MS = 48 * STUNDE_MS
|
||||
const VERWORFEN_BEHALTEN_MS = 48 * STUNDE_MS
|
||||
/** bookingId der Mails ohne Buchung (G01 zu_viele): sie haben keinen Bestaetigungslink und verfallen nie. */
|
||||
const OHNE_BUCHUNG = '-'
|
||||
const VERWORFEN_GRUND = 'Verworfen: Bestätigungslink abgelaufen'
|
||||
/** Laenger als das in `wartet` (ab createdAt) ist ein Eintrag festgefahren (beendeFestgefahrene). */
|
||||
const WARTEN_MAX_MS = 48 * STUNDE_MS
|
||||
const ICS_METHODEN = ['REQUEST', 'CANCEL', 'PUBLISH']
|
||||
@@ -76,6 +87,8 @@ export function mailKlasse(art) {
|
||||
let pauseBisMs = 0
|
||||
/** Belegte Plaetze im letzten Stundenfenster: { t: Sendezeit (ms), f: featureId, n: nachrangig }. */
|
||||
let plaetze = []
|
||||
/** Veraltete G01, die dieser Prozess verworfen hat (verworfeneG01). */
|
||||
let verworfenG01 = 0
|
||||
/** Laufende sendeEintrag-Aufrufe (fuer warteAufVersand). */
|
||||
const laufend = new Set()
|
||||
/** Eintraege, die schon rausgingen, deren Ergebnis aber nicht gespeichert werden konnte: id => Versandzeitpunkt (ms). */
|
||||
@@ -220,10 +233,10 @@ export async function einreihen(
|
||||
* laeuft und Stundenbudget, Vorrang-Reserve und Kontingent der Funktion reichen (pruefePlatz).
|
||||
* @param {string} id
|
||||
* @param {Date} [now]
|
||||
* @returns {Promise<'gesendet'|'wartet'|'fehler'|'uebersprungen'>} gesendet = dieser Aufruf hat die Mail gesendet;
|
||||
* @returns {Promise<'gesendet'|'wartet'|'fehler'|'verworfen'|'uebersprungen'>} gesendet = dieser Aufruf hat die Mail gesendet;
|
||||
* wartet = Eintrag bleibt in der Warteschlange (Pause, Budget, Reserve, Kontingent, noch nicht faellig, vorlaeufiger
|
||||
* Fehler); fehler = dieser Aufruf hat ihn endgueltig scheitern lassen; uebersprungen = Eintrag fehlt oder ist schon
|
||||
* gesendet/fehlgeschlagen
|
||||
* Fehler); fehler = dieser Aufruf hat ihn endgueltig scheitern lassen; verworfen = veraltete G01 (Bestaetigungslink
|
||||
* abgelaufen), nicht gesendet; uebersprungen = Eintrag fehlt oder ist schon gesendet/fehlgeschlagen/verworfen
|
||||
*/
|
||||
export function sendeEintrag(id, now = new Date()) {
|
||||
const lauf = sende(id, now).finally(() => laufend.delete(lauf))
|
||||
@@ -252,6 +265,11 @@ async function sende(id, now) {
|
||||
await speichereGesendet(doc, unverbucht.get(id))
|
||||
return 'gesendet'
|
||||
}
|
||||
// Vor dem Budget: eine veraltete G01 braucht keinen Platz und keinen Sendeversuch
|
||||
if (istVeraltetG01(doc, nowMs)) {
|
||||
await speichereVerworfen(doc, nowMs)
|
||||
return 'verworfen'
|
||||
}
|
||||
if (new Date(doc.naechsterVersuch).getTime() > nowMs) return 'wartet'
|
||||
// Der Platz wird ohne Zwischenschritt vor dem Senden belegt: gleichzeitige Versuche koennen Budget und Kontingent nicht
|
||||
// ueberziehen. Ein gebremster Eintrag bleibt unveraendert `wartet` (kein Versuch).
|
||||
@@ -278,6 +296,30 @@ async function sende(id, now) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Ist der Bestaetigungslink dieser G01 schon verfallen? Das Halte-Fenster (config.termine.holdMinutes, bei jedem Aufruf
|
||||
* gelesen) plus eine Minute Toleranz ab Einreihen; genau so alt ist noch nicht veraltet. G01 ohne Buchung (zu_viele) hat
|
||||
* keinen Link, andere Arten verfallen nie.
|
||||
*/
|
||||
function istVeraltetG01(doc, nowMs) {
|
||||
if (doc.art !== 'G01' || doc.bookingId === OHNE_BUCHUNG) return false
|
||||
return nowMs - zeitMs(doc.createdAt) > (config.termine.holdMinutes + 1) * MIN_MS
|
||||
}
|
||||
|
||||
/** Veraltete G01 verwerfen: Status `verworfen`, personenbezogene Felder leeren, Loeschtermin jetzt + 48 Stunden (raeumeAuf). */
|
||||
async function speichereVerworfen(doc, nowMs) {
|
||||
await updateDocument(col(), doc.$id, {
|
||||
status: 'verworfen',
|
||||
an: '',
|
||||
html: '',
|
||||
text: '',
|
||||
ics: '',
|
||||
fehler: VERWORFEN_GRUND,
|
||||
naechsterVersuch: iso(nowMs + VERWORFEN_BEHALTEN_MS),
|
||||
})
|
||||
verworfenG01 += 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Nach dem Versand: Status, Zeiten setzen und personenbezogene Felder leeren. Scheitert das Speichern (auch beim
|
||||
* zweiten Versuch), merkt sich der Prozess den Versand, damit der Eintrag nicht noch einmal gesendet wird.
|
||||
@@ -381,10 +423,10 @@ async function verarbeiteKlasse(klasse, nowMs, now, lauf) {
|
||||
}
|
||||
lauf.versucht.add(eintrag.$id)
|
||||
try {
|
||||
if ((await sendeEintrag(eintrag.$id, now)) === 'gesendet') {
|
||||
lauf.gesendet += 1
|
||||
bewegt = true
|
||||
}
|
||||
const ergebnis = await sendeEintrag(eintrag.$id, now)
|
||||
if (ergebnis === 'gesendet') lauf.gesendet += 1
|
||||
// auch ein verworfener Eintrag verlaesst die Liste (`wartet`); so kommen die Eintraege dahinter in der naechsten Runde dran
|
||||
if (ergebnis === 'gesendet' || ergebnis === 'verworfen') bewegt = true
|
||||
} catch (err) {
|
||||
console.error(`[termine-outbox] Eintrag ${eintrag.$id}: ${err.message}`)
|
||||
}
|
||||
@@ -421,8 +463,10 @@ export async function warteAufVersand() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Buchungen (aus bookingIds) mit mindestens einer endgueltig gescheiterten Mail (Status fehler). Das Portal zeigt dort
|
||||
* Buchungen (aus bookingIds) mit mindestens einer endgueltig gescheiterten Mail (Status fehler; verworfene veraltete G01
|
||||
* zaehlen nicht: der Betrieb sieht fuer einen verfallenen Halte-Eintrag keinen Zustellfehler). Das Portal zeigt dort
|
||||
* „Bestätigung konnte nicht zugestellt werden“. Die Eintraege verschwinden mit raeumeAuf (48 Stunden nach dem Fehler).
|
||||
* Fragt ueber den Index idx_booking (bookingId) ab.
|
||||
* @param {string[]} bookingIds
|
||||
* @returns {Promise<Set<string>>}
|
||||
*/
|
||||
@@ -437,14 +481,14 @@ export async function zustellFehler(bookingIds) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Taeglicher Job: loescht gesendete Eintraege 7 Tage nach dem Versand und fehlgeschlagene 48 Stunden nach dem Fehler
|
||||
* (beides steht in naechsterVersuch). Wartende Eintraege bleiben.
|
||||
* Taeglicher Job: loescht gesendete Eintraege 7 Tage nach dem Versand, fehlgeschlagene 48 Stunden nach dem Fehler und
|
||||
* verworfene (veraltete G01) 48 Stunden nach dem Verwerfen (alles steht in naechsterVersuch). Wartende Eintraege bleiben.
|
||||
* @param {Date} now
|
||||
* @returns {Promise<number>} Anzahl geloeschter Eintraege
|
||||
*/
|
||||
export async function raeumeAuf(now) {
|
||||
const nowMs = pruefeNow(now, 'raeumeAuf')
|
||||
const alt = await listAllDocuments(col(), [Query.equal('status', ['gesendet', 'fehler']), Query.lessThanEqual('naechsterVersuch', iso(nowMs))])
|
||||
const alt = await listAllDocuments(col(), [Query.equal('status', ['gesendet', 'fehler', 'verworfen']), Query.lessThanEqual('naechsterVersuch', iso(nowMs))])
|
||||
let geloescht = 0
|
||||
for (const eintrag of alt) {
|
||||
try {
|
||||
@@ -461,7 +505,7 @@ export async function raeumeAuf(now) {
|
||||
* Gastdaten loeschen (Portal "Gastdaten löschen"): noch nicht versandte Mails einer Buchung (Status wartet) werden
|
||||
* geloescht, denn sie halten Adresse und Inhalt. Je Eintrag unter der Sendesperre `termine-mail:<id>` nach neuem Lesen:
|
||||
* ein Eintrag, der inzwischen gesendet wurde (seine Felder sind dann schon leer), bleibt; ein inzwischen geloeschter
|
||||
* stoert nicht. Gesendete und fehlgeschlagene Eintraege behandelt raeumeAuf.
|
||||
* stoert nicht. Gesendete, fehlgeschlagene und verworfene Eintraege behandelt raeumeAuf. Fragt ueber den Index idx_booking ab.
|
||||
* @param {string} bookingId
|
||||
* @returns {Promise<number>} Anzahl geloeschter Eintraege
|
||||
*/
|
||||
@@ -528,14 +572,20 @@ export async function beendeFestgefahrene(now) {
|
||||
return beendet
|
||||
}
|
||||
|
||||
/** Wie viele veraltete G01 dieser Prozess seit dem Start verworfen hat (Betriebsmeldung, A10b). */
|
||||
export function verworfeneG01() {
|
||||
return verworfenG01
|
||||
}
|
||||
|
||||
/** Ende der letzten Anmeldepause (Date) oder null, wenn es noch keine gab. Eine Pause gilt, solange das Ende nach `now` liegt. */
|
||||
export function pausiertBis() {
|
||||
return pauseBisMs ? new Date(pauseBisMs) : null
|
||||
}
|
||||
|
||||
/** Nur fuer Tests: Pause, Budgetfenster und nicht gespeicherte Versände zuruecksetzen. */
|
||||
/** Nur fuer Tests: Pause, Budgetfenster, nicht gespeicherte Versände und den Zaehler verworfener G01 zuruecksetzen. */
|
||||
export function resetOutboxForTests() {
|
||||
pauseBisMs = 0
|
||||
plaetze = []
|
||||
unverbucht.clear()
|
||||
verworfenG01 = 0
|
||||
}
|
||||
@@ -114,7 +114,8 @@ export const TERMINE_COLLECTIONS = {
|
||||
str('fehler', 300),
|
||||
dt('createdAt'),
|
||||
],
|
||||
indexes: [index('idx_status_next', 'status', 'naechsterVersuch')],
|
||||
// idx_status_next: Minutenjob und Aufraeumen; idx_booking: zustellFehler und verwerfeWartende fragen nach der Buchung
|
||||
indexes: [index('idx_status_next', 'status', 'naechsterVersuch'), index('idx_booking', 'bookingId')],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user