Terminplaner: Aufbewahrungs-Loeschung je Funktion mit Deckel, Sicherheitsnetz und Bericht
erledigt/nichterschienen werden jetzt je Funktion (portalFeatures termine, active|disabled) nach deren aufbewahrungTage seitenweise geloescht; beide Status teilen sich den Deckel loeschDeckel der Funktion, alle Schritte eines Laufs zusammen loeschDeckelGesamt (neu, Standard 20000). Ein Sicherheitsnetz loescht erledigt/nichterschienen ohne Funktionsfilter nach 365 Tagen (Buchungen ohne Funktionsdokument, unlesbare Einstellung). Der Tagesbericht nennt "Loeschlaeufe am Deckel: n, Rest folgt an den nächsten Tagen" nur bei gedeckelt > 0. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c95dfc322
commit
9b4305e5de
4 files changed
+423
-43
No files matched your search
+4
-1
@@ -386,8 +386,11 @@ export const config = {
|
||||
holdMinutes: 30,
|
||||
zelleMin: 5,
|
||||
mailsProStunde: 200,
|
||||
// Hoechstens so viele Buchungen loescht ein Schritt der Aufbewahrungs-Loeschung je Lauf (Tagesjob); der Rest folgt am naechsten Tag
|
||||
// Hoechstens so viele Buchungen loescht ein Schritt der Aufbewahrungs-Loeschung je Lauf (Tagesjob): je Status, bei erledigt und
|
||||
// nichterschienen zusammen je Funktion; der Rest folgt am naechsten Tag
|
||||
loeschDeckel: 2000,
|
||||
// Alle Schritte der Aufbewahrungs-Loeschung eines Laufs zusammen loeschen hoechstens so viele Buchungen (Gesamtdeckel ueber alle Funktionen)
|
||||
loeschDeckelGesamt: 20000,
|
||||
// Mindestzeit zwischen Laden und Absenden des Buchungsformulars (schneller = Bot)
|
||||
formMinSekunden: nonNegativeNumber(process.env.TERMINE_FORM_MIN_SECONDS, 3),
|
||||
// Kuerzeste Antwortzeit (ms) von POST /buchungen und den Widerrufs-POSTs der oeffentlichen Schnittstelle: verwischt, ob
|
||||
|
||||
@@ -42,8 +42,9 @@ import { gibZellenFrei, raeumeVerwaisteZellen } from './zellen.js'
|
||||
* ergeben genau einen Endzustand. Mails gehen erst nach der Aenderung und ausserhalb der Sperre in die Warteschlange.
|
||||
* - Zellen gibt nur zellen.js frei (gibZellenFrei / raeumeVerwaisteZellen, jeweils unter der Ressourcensperre).
|
||||
* - Loeschlaeufe (Aufbewahrung) arbeiten seitenweise (arbeiteSeiten): eine Seite hoechstens 100 Buchungen ohne Gastfelder,
|
||||
* Frist in der Abfrage, danach neu lesen. Ein Schritt loescht hoechstens config.termine.loeschDeckel Buchungen je Lauf; der
|
||||
* Rest folgt am naechsten Tag und wird gezaehlt (`gedeckelt`), nie still uebersprungen.
|
||||
* Frist in der Abfrage, danach neu lesen. Ein Schritt loescht hoechstens config.termine.loeschDeckel Buchungen je Lauf (erledigt
|
||||
* und nichterschienen zusammen je Funktion), alle Schritte eines Laufs zusammen hoechstens config.termine.loeschDeckelGesamt;
|
||||
* der Rest folgt am naechsten Tag und wird gezaehlt (`gedeckelt`), nie still uebersprungen.
|
||||
*/
|
||||
|
||||
const MIN_MS = 60 * 1000
|
||||
@@ -136,7 +137,8 @@ async function mitFrischer(lauf, was, id, fn) {
|
||||
* alles auf einmal lesen duerfen. Der Lauf endet
|
||||
* - bei einer leeren Seite (alles erledigt),
|
||||
* - am Deckel: sind `deckel` Eintraege geloescht und es kommt noch einer, zaehlt lauf.gedeckelt, eine Warnung ohne Kennungen
|
||||
* steht im Log und der Rest folgt im naechsten Lauf,
|
||||
* steht im Log und der Rest folgt im naechsten Lauf (`deckel` 0 = nur nachsehen, ob noch etwas ansteht: eine leere Seite endet
|
||||
* ohne Zaehlung, sonst zaehlt es als Deckel); `deckelText` benennt den Deckel in der Warnung,
|
||||
* - bei einer Seite ohne einen bearbeiteten Eintrag (alle gescheitert oder schon versucht): die Fehler stehen im Log; eine Menge
|
||||
* merkt die versuchten IDs, damit ein Eintrag, der sich nicht bearbeiten laesst, keinen Endlosdurchlauf ausloest.
|
||||
* holeSeite() => Eintraege mit $id (hoechstens eine Seite); bearbeite(eintrag) => true (geloescht, zaehlt zum Deckel) | false (nichts
|
||||
@@ -144,8 +146,8 @@ async function mitFrischer(lauf, was, id, fn) {
|
||||
* Log und der Lauf endet unvollstaendig.
|
||||
* => { geloescht, gedeckelt (am Deckel beendet), vollstaendig (leere Seite erreicht und kein Fehler in diesem Lauf) }
|
||||
*/
|
||||
async function arbeiteSeiten(lauf, { holeSeite, bearbeite, deckel, was = 'Löschung' }) {
|
||||
if (!Number.isInteger(deckel) || deckel < 1) throw new TypeError(`arbeiteSeiten: deckel muss eine ganze Zahl ab 1 sein, nicht ${deckel}`)
|
||||
async function arbeiteSeiten(lauf, { holeSeite, bearbeite, deckel, was = 'Löschung', deckelText = `von ${deckel} je Lauf` }) {
|
||||
if (!Number.isInteger(deckel) || deckel < 0) throw new TypeError(`arbeiteSeiten: deckel muss eine ganze Zahl ab 0 sein, nicht ${deckel}`)
|
||||
const versucht = new Set()
|
||||
let geloescht = 0
|
||||
let fehlerBeiEintrag = false
|
||||
@@ -164,7 +166,7 @@ async function arbeiteSeiten(lauf, { holeSeite, bearbeite, deckel, was = 'Lösch
|
||||
if (versucht.has(eintrag.$id)) continue
|
||||
if (geloescht >= deckel) {
|
||||
lauf.gedeckelt += 1
|
||||
console.warn(`[termine-jobs] ${lauf.job}: ${was} endet am Deckel von ${deckel} je Lauf, der Rest folgt an den nächsten Tagen`)
|
||||
console.warn(`[termine-jobs] ${lauf.job}: ${was} endet am Deckel ${deckelText}, der Rest folgt an den nächsten Tagen`)
|
||||
return { geloescht, gedeckelt: true, vollstaendig: false }
|
||||
}
|
||||
versucht.add(eintrag.$id)
|
||||
@@ -351,53 +353,97 @@ function loescheMitZellen(lauf, id, status, frist = null) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Aufbewahrungs-Loeschung (Spezifikation 11) fuer Buchungen. Durchgang (1): verfallen (48 Stunden nach heldUntil), abgesagt und
|
||||
* abgelehnt (30 Tage nach der letzten Aenderung) ueber alle Funktionen, je Status seitenweise (arbeiteSeiten) bis zum Deckel
|
||||
* config.termine.loeschDeckel. Die Frist steht in der Abfrage; unter der Buchungssperre wird Status und Bezugszeit noch einmal
|
||||
* frisch geprueft. erledigt/nichterschienen laufen bis zum Umbau je Funktion noch ueber den bisherigen Weg (eine Liste, die Frist
|
||||
* der Funktion am Kandidaten).
|
||||
* Aufbewahrungs-Loeschung (Spezifikation 11) fuer Buchungen in drei Durchgaengen, jeweils seitenweise (arbeiteSeiten):
|
||||
* (1) verfallen (48 Stunden nach heldUntil), abgesagt und abgelehnt (30 Tage nach der letzten Aenderung) ueber alle Funktionen,
|
||||
* (2) erledigt und nichterschienen je Funktion (portalFeatures featureKey termine, active oder disabled; nur die IDs werden
|
||||
* gelesen) nach deren aufbewahrungTage (Vorgabe 90). Ist die Einstellung einer Funktion nicht lesbar, bleibt sie in diesem
|
||||
* Lauf unberuehrt (nie frueher loeschen als eingestellt),
|
||||
* (3) Sicherheitsnetz: erledigt und nichterschienen ohne Funktionsfilter nach 365 Tagen, der groessten erlaubten Aufbewahrung.
|
||||
* Es faengt Buchungen ohne Funktionsdokument und Funktionen mit unlesbarer Einstellung spaetestens dann (Buchungen ohne
|
||||
* Funktionsdokument bleiben bis dahin, Waisen gibt es in Produktion nicht).
|
||||
* Die Frist steht in der Abfrage; unter der Buchungssperre wird Status und Bezugszeit noch einmal frisch geprueft (zweiter Boden).
|
||||
* Deckel: ein Schritt loescht hoechstens config.termine.loeschDeckel Buchungen (erledigt und nichterschienen einer Funktion
|
||||
* zusammen), alle Schritte des Laufs zusammen hoechstens config.termine.loeschDeckelGesamt. Jeder Schritt, der daran endet, zaehlt
|
||||
* in `gedeckelt` (Warnung ohne Kennungen im Log); der Rest folgt im naechsten Lauf. Endet ein Schritt am Gesamtdeckel, ist der Lauf
|
||||
* zu Ende: die uebrigen Schritte folgen am naechsten Tag. Ist der Gesamtdeckel genau erreicht, sieht jeder weitere Schritt nur nach,
|
||||
* ob noch etwas ansteht (leere Seite = nicht gedeckelt).
|
||||
* => Loeschzaehler je Status und `gedeckelt` = Zahl der Schritte, die am Deckel endeten
|
||||
*/
|
||||
async function loeschfristen(lauf, now) {
|
||||
const geloescht = { verfallen: 0, abgesagt: 0, abgelehnt: 0, erledigt: 0, nichterschienen: 0 }
|
||||
const deckelJeSchritt = config.termine.loeschDeckel
|
||||
const deckelGesamt = config.termine.loeschDeckelGesamt
|
||||
let gedeckelt = 0
|
||||
const tage = new Map()
|
||||
const tageFuer = (featureId) => {
|
||||
if (!tage.has(featureId)) tage.set(featureId, aufbewahrungTage(lauf, featureId))
|
||||
return tage.get(featureId)
|
||||
let rest = deckelGesamt // was vom Gesamtdeckel noch uebrig ist
|
||||
let amGesamtdeckel = false // ein Schritt endete am Gesamtdeckel: keine weiteren Schritte in diesem Lauf
|
||||
|
||||
/**
|
||||
* Ein Loeschschritt fuer Buchungen eines Status mit Bezugszeit vor `vor` (ms), optional nur einer Funktion, unter beiden Deckeln:
|
||||
* hoechstens `eigener`, hoechstens der Rest des Gesamtdeckels. => { geloescht, gedeckelt }
|
||||
*/
|
||||
const loescheSeiten = async (was, status, eigener, eigenerText, vor, featureId = null) => {
|
||||
if (amGesamtdeckel) return { geloescht: 0, gedeckelt: false }
|
||||
const amGesamt = rest <= eigener
|
||||
const frist = { feld: loeschBezugsfeld(status), vor }
|
||||
const erg = await schritt(
|
||||
lauf,
|
||||
was,
|
||||
() =>
|
||||
arbeiteSeiten(lauf, {
|
||||
was,
|
||||
deckel: amGesamt ? rest : eigener,
|
||||
deckelText: amGesamt ? `von ${deckelGesamt} für alle Schritte des Laufs zusammen (Gesamtdeckel)` : eigenerText,
|
||||
holeSeite: () => loeschKandidaten(status, new Date(vor), featureId === null ? {} : { featureId }),
|
||||
bearbeite: async (kandidat) => {
|
||||
const weg = await loescheMitZellen(lauf, kandidat.$id, status, frist)
|
||||
if (weg) geloescht[status] += 1
|
||||
return weg
|
||||
},
|
||||
}),
|
||||
{ geloescht: 0, gedeckelt: false },
|
||||
)
|
||||
rest -= erg.geloescht
|
||||
if (erg.gedeckelt) {
|
||||
gedeckelt += 1
|
||||
if (amGesamt) amGesamtdeckel = true
|
||||
}
|
||||
return erg
|
||||
}
|
||||
|
||||
// Durchgang (1): verfallen, abgesagt, abgelehnt
|
||||
for (const [status, behalten] of [
|
||||
['verfallen', VERFALLEN_BEHALTEN_MS],
|
||||
['abgesagt', ABGESAGT_BEHALTEN_MS],
|
||||
['abgelehnt', ABGESAGT_BEHALTEN_MS],
|
||||
]) {
|
||||
await schritt(lauf, `Löschfrist ${status}`, async () => {
|
||||
const vor = lauf.nowMs - behalten
|
||||
const frist = { feld: loeschBezugsfeld(status), vor }
|
||||
const erg = await arbeiteSeiten(lauf, {
|
||||
was: `Löschfrist ${status}`,
|
||||
deckel: config.termine.loeschDeckel,
|
||||
holeSeite: () => loeschKandidaten(status, new Date(vor)),
|
||||
bearbeite: async (kandidat) => {
|
||||
const weg = await loescheMitZellen(lauf, kandidat.$id, status, frist)
|
||||
if (weg) geloescht[status] += 1
|
||||
return weg
|
||||
},
|
||||
})
|
||||
if (erg.gedeckelt) gedeckelt += 1
|
||||
})
|
||||
await loescheSeiten(`Löschfrist ${status}`, status, deckelJeSchritt, undefined, lauf.nowMs - behalten)
|
||||
}
|
||||
|
||||
// Bisheriger Weg fuer erledigt/nichterschienen: Kandidaten ab der fruehesten moeglichen Frist, dann entscheidet die Frist der Funktion
|
||||
// Durchgang (2): erledigt/nichterschienen je Funktion nach deren Aufbewahrung, die beiden Status teilen sich den Deckel der Funktion
|
||||
const funktionen = amGesamtdeckel
|
||||
? []
|
||||
: await schritt(
|
||||
lauf,
|
||||
'Löschfrist: Funktionen nicht gelesen',
|
||||
() => listAllDocuments(config.collections.portalFeatures, [Query.equal('featureKey', 'termine'), Query.equal('state', ['active', 'disabled']), Query.select(['$id'])]),
|
||||
[],
|
||||
)
|
||||
for (const { $id: featureId } of funktionen) {
|
||||
if (amGesamtdeckel) break
|
||||
const tage = await aufbewahrungTage(lauf, featureId)
|
||||
if (tage === null) continue
|
||||
const vor = lauf.nowMs - tage * TAG_MS
|
||||
let geloeschtJeFunktion = 0
|
||||
for (const status of ['erledigt', 'nichterschienen']) {
|
||||
const erg = await loescheSeiten(`Löschfrist ${status} (je Funktion)`, status, deckelJeSchritt - geloeschtJeFunktion, `von ${deckelJeSchritt} je Funktion`, vor, featureId)
|
||||
geloeschtJeFunktion += erg.geloescht
|
||||
if (erg.gedeckelt) break // die Funktion endete am Deckel, einmal gezaehlt
|
||||
}
|
||||
}
|
||||
|
||||
// Durchgang (3): Sicherheitsnetz, die groesste erlaubte Aufbewahrung gilt fuer jede Buchung, auch ohne lesbare Funktion
|
||||
for (const status of ['erledigt', 'nichterschienen']) {
|
||||
await schritt(lauf, `Löschfrist ${status}`, async () => {
|
||||
for (const kandidat of await loeschKandidaten(status, new Date(lauf.nowMs - AUFBEWAHRUNG_MIN * TAG_MS))) {
|
||||
const t = await tageFuer(kandidat.featureId)
|
||||
if (t === null || !(zeitMs(kandidat.endsAt) < lauf.nowMs - t * TAG_MS)) continue
|
||||
if (await loescheMitZellen(lauf, kandidat.$id, status)) geloescht[status] += 1
|
||||
}
|
||||
})
|
||||
await loescheSeiten(`Löschfrist ${status} (Sicherheitsnetz)`, status, deckelJeSchritt, undefined, lauf.nowMs - AUFBEWAHRUNG_MAX * TAG_MS)
|
||||
}
|
||||
return { ...geloescht, gedeckelt }
|
||||
}
|
||||
@@ -480,8 +526,9 @@ async function loescheAbgeschaltete(lauf) {
|
||||
|
||||
/**
|
||||
* Loeschfristen (Spezifikation 11) fuer Buchungen: verfallen 48 Stunden nach heldUntil, abgesagt/abgelehnt 30 Tage nach der letzten
|
||||
* Aenderung, erledigt/nichterschienen aufbewahrungTage der Funktion (Vorgabe 90) nach Terminende; jeweils samt Zellen, je Schritt
|
||||
* hoechstens config.termine.loeschDeckel (der Rest folgt im naechsten Lauf). Baustein des Tagesjobs, ohne Schalter- und
|
||||
* Aenderung, erledigt/nichterschienen aufbewahrungTage der Funktion (Vorgabe 90) nach Terminende, spaetestens nach 365 Tagen
|
||||
* (Sicherheitsnetz); jeweils samt Zellen, je Schritt hoechstens config.termine.loeschDeckel und je Lauf hoechstens
|
||||
* config.termine.loeschDeckelGesamt (der Rest folgt im naechsten Lauf). Baustein des Tagesjobs, ohne Schalter- und
|
||||
* Ueberlappungspruefung (fuer Tests).
|
||||
* @param {Date} now
|
||||
* @returns {Promise<{ verfallen: number, abgesagt: number, abgelehnt: number, erledigt: number, nichterschienen: number, gedeckelt: number }>}
|
||||
@@ -531,11 +578,13 @@ export function tagesbericht(ergebnis) {
|
||||
const l = ergebnis.loeschfristen
|
||||
const a = ergebnis.abgeschaltet
|
||||
const gesamt = Object.values(l).reduce((summe, n) => summe + n, 0) + a.buchungen
|
||||
const amDeckel = Object.values(ergebnis.gedeckelt ?? {}).reduce((summe, n) => summe + n, 0)
|
||||
const deckel = amDeckel > 0 ? `Löschläufe am Deckel: ${amDeckel}, Rest folgt an den nächsten Tagen, ` : ''
|
||||
return (
|
||||
`[termine] Tagesjob: ${gesamt} Buchungen gelöscht (verfallen ${l.verfallen}, abgesagt ${l.abgesagt}, abgelehnt ${l.abgelehnt}, ` +
|
||||
`erledigt ${l.erledigt}, nichterschienen ${l.nichterschienen}, abgeschaltete Funktionen ${a.buchungen}), ` +
|
||||
`${a.funktionen} Funktionen bereinigt, ${ergebnis.zellen} Zellen, ${ergebnis.aufgeraeumt} Outbox, ` +
|
||||
`${ergebnis.festgefahren} Mails nicht zugestellt, ${ergebnis.fehler} Fehler`
|
||||
`${ergebnis.festgefahren} Mails nicht zugestellt, ${deckel}${ergebnis.fehler} Fehler`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user