Terminplaner: Zellen-Sweep seitenweise ueber idx_bis mit Deckel

raeumeVerwaisteZellen(now, { deckel = config.termine.zellenDeckel }) liest
Zellen mit bis < jetzt - 1 Tag in Seiten zu 100 ueber idx_bis (aelteste
zuerst, ohne Cursor neu gelesen), gruppiert je Ressourcensperre mit Map und
loescht unter der Sperre nur, was dort noch dieselbe Buchung hat und noch alt
ist. Die Buchungen werden nicht gelesen. Der Lauf endet am Deckel (5000,
gedeckelt wird im Tagesjob gezaehlt und im Log gewarnt), bei einer leeren
Seite oder einer Seite ohne Erfolg; der erste Fehler wird am Ende geworfen.
ungueltigeZellen prueft zuerst das Zeitfenster (auch fuer belegeZellen).
verwaisteZellen, alleZellen und ALLE_ZELLEN_MAX entfallen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 09:44:47 +00:00
1 parent d040a55f89
commit 978722023f
6 files changed
+404 -158

No files matched your search

+69 -4
View File
@@ -51,6 +51,7 @@ const ABSENDER_VORHER = config.smtp.from
const MODUS_VORHER = config.termine.mode
const DECKEL_VORHER = config.termine.loeschDeckel
const GESAMT_DECKEL_VORHER = config.termine.loeschDeckelGesamt
const ZELLEN_DECKEL_VORHER = config.termine.zellenDeckel
const BASIS_VORHER = config.publicBaseUrl
const iso = (wert) => new Date(wert).toISOString()
@@ -134,6 +135,7 @@ afterEach(async () => {
config.termine.mode = MODUS_VORHER
config.termine.loeschDeckel = DECKEL_VORHER
config.termine.loeschDeckelGesamt = GESAMT_DECKEL_VORHER
config.termine.zellenDeckel = ZELLEN_DECKEL_VORHER
config.smtp.from = ABSENDER_VORHER
config.publicBaseUrl = BASIS_VORHER
assert.doesNotMatch(text, /Mustermann|max@example\.com/i, 'kein Gastname und keine Gastadresse im Log')
@@ -928,9 +930,9 @@ test('wendeLoeschfristenAn: der Deckel gilt je Status; genau so viele fällige w
test('wendeLoeschfristenAn: scheitert jedes Löschen, endet der Lauf nach einer Seite je Schritt, zählt die Fehler, lässt die anderen Schritte laufen; der nächste Lauf holt alles nach', async () => {
for (const art of ARTEN) for (let i = 0; i < 30; i += 1) await legeLoeschFall(`b_${KUERZEL[art]}${nr(i)}`, art, 1 + i * 1000)
await legeLoeschFall('b_ver_jung', 'verfallen', -1)
// andere Schritte des Tagesjobs: eine alte, versandte Mail und eine verwaiste Zelle
// andere Schritte des Tagesjobs: eine alte, versandte Mail und eine verwaiste Zelle (Buchung fehlt, Ende mehr als einen Tag vorbei: nur solche findet der Zellen-Sweep)
legeEintrag('o_gesendet', { status: 'gesendet', an: '', html: '', text: '', naechsterVersuch: iso(plus(JETZT, -MIN)) })
await belegeZellen({ featureId: F1, ressourceId: 'r_geist', bookingId: 'b_geist', start: plus(JETZT, 2 * TAG), blockEnde: plus(JETZT, 2 * TAG + 30 * MIN), now: JETZT })
await belegeZellen({ featureId: F1, ressourceId: 'r_geist', bookingId: 'b_geist', start: plus(JETZT, -3 * TAG), blockEnde: plus(JETZT, -3 * TAG + 30 * MIN), now: JETZT })
let erg
const zaehler = await mitScheiternderLoeschung(
@@ -1629,8 +1631,8 @@ test('taeglicherJob: Löschfristen, Warteschlange, festgefahrene Mails, verwaist
await legeBuchung('b_erledigt', { status: 'erledigt', ende: plus(JETZT, -100 * TAG) })
await legeBuchung('b_zukunft', { start: plus(JETZT, 5 * TAG) })
await legeBuchung('b_abgeschaltet', { featureId: F2, start: plus(JETZT, 5 * TAG) })
// verwaiste Zelle: Buchung fehlt
await belegeZellen({ featureId: F1, ressourceId: 'r_geist', bookingId: 'b_geist', start: plus(JETZT, 2 * TAG), blockEnde: plus(JETZT, 2 * TAG + 30 * MIN), now: JETZT })
// verwaiste Zelle: Buchung fehlt, Ende mehr als einen Tag vorbei (nur solche findet der Zellen-Sweep)
await belegeZellen({ featureId: F1, ressourceId: 'r_geist', bookingId: 'b_geist', start: plus(JETZT, -3 * TAG), blockEnde: plus(JETZT, -3 * TAG + 30 * MIN), now: JETZT })
legeEintrag('o_gesendet', { status: 'gesendet', an: '', html: '', text: '', naechsterVersuch: iso(plus(JETZT, -MIN)) })
legeEintrag('o_festgefahren', { createdAt: iso(plus(JETZT, -3 * TAG)) })
@@ -1688,6 +1690,69 @@ test('taeglicherJob: abgeschaltete Funktion am Deckel ⇒ gedeckelt.abgeschaltet
assert.equal(aw.docs('termineConfig').get(F1), undefined)
})
/** Alte, verwaiste Zellen (Buchung fehlt, Ende mehr als einen Tag vorbei), n Stück, die erste ist die älteste. */
function legeAlteZellen(n, praefix = 'c_alt') {
for (let i = 0; i < n; i += 1) {
const bis = plus(JETZT, -3 * TAG - (n - i) * 1000)
aw.seed('termineClaims', {
$id: `${praefix}${String(i).padStart(3, '0')}`,
featureId: F1,
ressourceId: `r_geist${i % 3}`,
bookingId: `b_geist${i}`,
zelle: iso(plus(bis, -5 * MIN)),
bis: iso(bis),
})
}
}
test('taeglicherJob: Zellen-Sweep am Deckel ⇒ gedeckelt.zellen zählt, zellen bleibt die Zahl der gelöschten Zellen, der Rest folgt an den nächsten Tagen', async () => {
legeAlteZellen(600)
config.termine.zellenDeckel = 250
let erg = await taeglicherJob(JETZT)
assert.equal(erg.zellen, 250)
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 1 })
assert.equal(erg.fehler, 0)
assert.equal(aw.docs('termineClaims').size, 350)
assert.equal(warnLog().filter((zeile) => /Deckel/.test(zeile)).length, 1)
assert.doesNotMatch(warnLog().join('\n'), /\bb_|\bf_|\br_|\bc_/, 'die Warnung nennt keine Kennungen')
const zeile = tagesbericht(erg)
assert.match(zeile, /, 250 Zellen, /)
assert.match(zeile, /Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, 0 Fehler$/)
erg = await taeglicherJob(JETZT)
assert.equal(erg.zellen, 250)
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 1 })
assert.equal(aw.docs('termineClaims').size, 100)
log.warn.mock.resetCalls()
erg = await taeglicherJob(JETZT)
assert.equal(erg.zellen, 100)
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
assert.equal(aw.docs('termineClaims').size, 0)
assert.equal(warnLog().filter((zeile) => /Deckel/.test(zeile)).length, 0)
assert.doesNotMatch(tagesbericht(erg), /Deckel/)
})
test('taeglicherJob: Zellen-Sweep mit Standard-Deckel ⇒ zellen ist die Zahl, gedeckelt.zellen 0; nicht lesbare Zellen zählen als Fehler, die Zahlen bleiben Zahlen', async () => {
legeAlteZellen(130)
let erg = await taeglicherJob(JETZT)
assert.equal(erg.zellen, 130)
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
assert.equal(erg.fehler, 0)
legeAlteZellen(5)
aw.failNext({ collection: 'termineClaims', method: 'GET', status: 500, message: 'Nachbau kaputt' }) // die erste Seite des Sweeps
erg = await taeglicherJob(JETZT)
assert.equal(erg.zellen, 0)
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
assert.equal(erg.fehler, 1)
assert.equal(aw.docs('termineClaims').size, 5)
erg = await taeglicherJob(JETZT)
assert.equal(erg.zellen, 5)
assert.equal(erg.fehler, 0)
})
test('taeglicherJob: ist die Liste der abgeschalteten Funktionen nicht lesbar, zählt der Schritt nichts, gedeckelt.abgeschaltet bleibt eine Zahl und die übrigen Schritte laufen', async () => {
await abgeschaltetMitBuchungen(3, { zellen: false })
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 1 }) // 1. GET = Funktionen der Löschfristen, 2. = abgeschaltete