Terminplaner: Jobs lesen abgeschaltete Funktionen vor dem Löschen neu, Tagesbericht im Log
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
485ff3bc15
commit
43ac05aa06
3 files changed
+182
-21
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { config } from '../../config.js'
|
||||
import { Query, deleteDocument, listAllDocuments } from '../appwriteAdmin.js'
|
||||
import { Query, deleteDocument, getDocument, listAllDocuments } from '../appwriteAdmin.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { gibFreiSicher, iso, vertragAm } from './ablauf.js'
|
||||
import {
|
||||
@@ -68,7 +68,7 @@ function melde(lauf, was, err, bookingId = null) {
|
||||
const zuletzt = letzteWarnung.get(lauf.job)
|
||||
if (zuletzt !== undefined && lauf.nowMs - zuletzt < STUNDE_MS) return
|
||||
letzteWarnung.set(lauf.job, lauf.nowMs)
|
||||
console.warn(`[termine-jobs] ${lauf.job}: ${err.type}, die Termine-Einrichtung ist unvollständig (Warnung höchstens einmal je Stunde)`)
|
||||
console.warn(`[termine-jobs] ${lauf.job}, Schritt „${was}“: ${err.type}, die Termine-Einrichtung ist unvollständig (Warnung höchstens einmal je Stunde)`)
|
||||
return
|
||||
}
|
||||
const nummer = bookingId ? ` (${buchungsNummer(bookingId)})` : ''
|
||||
@@ -314,9 +314,35 @@ async function loeschfristen(lauf, now) {
|
||||
return geloescht
|
||||
}
|
||||
|
||||
/** So viele Buchungen einer abgeschalteten Funktion hoechstens, bevor ihr Zustand erneut gelesen wird. */
|
||||
const ABGESCHALTET_NEU_PRUEFEN_ALLE = 50
|
||||
|
||||
/**
|
||||
* Gilt die Funktion unmittelbar vor einem unwiderruflichen Loeschschritt noch als abgeschaltet? Frisch gelesen, weil ein Wieder-
|
||||
* einschalten durch das Personal (siteFeatures.activate) weder sperrt noch disabledAt loescht: featureKey termine, state disabled
|
||||
* und disabledAt vor der Grenze. Sonst false mit Log, ohne Wurf – auch bei einem Lesefehler: im Zweifel wird nichts geloescht.
|
||||
*/
|
||||
async function nochAbgeschaltet(lauf, funktionId, grenze) {
|
||||
try {
|
||||
const frisch = await getDocument(config.collections.portalFeatures, funktionId)
|
||||
if (frisch.featureKey === 'termine' && frisch.state === 'disabled' && zeitMs(frisch.disabledAt) < grenze) return true
|
||||
console.warn(`[termine-jobs] ${lauf.job}: eine Funktion ist inzwischen wieder aktiv oder neu abgeschaltet, die Löschung wird für sie abgebrochen`)
|
||||
} catch (err) {
|
||||
if (err?.status === 404 && !fehlendeCollection(err)) {
|
||||
console.warn(`[termine-jobs] ${lauf.job}: eine Funktion wurde inzwischen entfernt, die Löschung wird für sie abgebrochen`)
|
||||
} else {
|
||||
melde(lauf, 'Funktion vor dem Löschen nicht gelesen', err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Funktionen im Zustand `disabled` seit mehr als 30 Tagen (disabledAt): alle Buchungen mit Zellen und zuletzt die Einstellung
|
||||
* loeschen. Scheitert eine Buchung, bleibt die Einstellung bis zum naechsten Lauf. Zellen ohne Buchung raeumt danach der Tagesjob.
|
||||
* loeschen. Vor der Buchungsliste, nach jeweils 50 Buchungen und vor dem Loeschen der Einstellung wird die Funktion frisch gelesen
|
||||
* (nochAbgeschaltet); gilt sie nicht mehr als abgeschaltet, endet die Loeschung fuer diese Funktion sofort (Log, kein Wurf).
|
||||
* Scheitert eine Buchung, bleibt die Einstellung bis zum naechsten Lauf. Zellen ohne Buchung raeumt danach der Tagesjob.
|
||||
* funktionen zaehlt die Funktionen, bei denen etwas geloescht wurde (Buchungen oder Einstellung).
|
||||
*/
|
||||
async function loescheAbgeschaltete(lauf) {
|
||||
const erg = { funktionen: 0, buchungen: 0 }
|
||||
@@ -324,26 +350,42 @@ async function loescheAbgeschaltete(lauf) {
|
||||
const funktionen = await listAllDocuments(config.collections.portalFeatures, [Query.equal('featureKey', 'termine'), Query.equal('state', 'disabled')])
|
||||
for (const funktion of funktionen) {
|
||||
if (!(zeitMs(funktion.disabledAt) < grenze)) continue
|
||||
erg.funktionen += 1
|
||||
const buchungen = await schritt(lauf, 'Buchungen abgeschalteter Funktion', () => listAllDocuments(config.collections.termineBookings, [Query.equal('featureId', funktion.$id)]), null)
|
||||
if (buchungen === null) continue
|
||||
if (buchungen === null || !(await nochAbgeschaltet(lauf, funktion.$id, grenze))) continue
|
||||
|
||||
let geloescht = 0
|
||||
let vollstaendig = true
|
||||
for (const buchung of buchungen) {
|
||||
for (const [i, buchung] of buchungen.entries()) {
|
||||
if (i > 0 && i % ABGESCHALTET_NEU_PRUEFEN_ALLE === 0 && !(await nochAbgeschaltet(lauf, funktion.$id, grenze))) {
|
||||
vollstaendig = false
|
||||
break
|
||||
}
|
||||
const weg = await loescheMitZellen(lauf, buchung.$id, null)
|
||||
if (weg) erg.buchungen += 1
|
||||
if (weg) geloescht += 1
|
||||
if (weg === undefined) vollstaendig = false
|
||||
}
|
||||
erg.buchungen += geloescht
|
||||
|
||||
let einstellungWeg = false
|
||||
if (vollstaendig) {
|
||||
await schritt(lauf, 'Einstellung abgeschalteter Funktion', () =>
|
||||
withLocks([`termine-cfg:${funktion.$id}`], async () => {
|
||||
try {
|
||||
await deleteDocument(config.collections.termineConfig, funktion.$id)
|
||||
} catch (err) {
|
||||
if (err?.status !== 404) throw err
|
||||
}
|
||||
}),
|
||||
einstellungWeg = await schritt(
|
||||
lauf,
|
||||
'Einstellung abgeschalteter Funktion',
|
||||
() =>
|
||||
withLocks([`termine-cfg:${funktion.$id}`], async () => {
|
||||
if (!(await nochAbgeschaltet(lauf, funktion.$id, grenze))) return false
|
||||
try {
|
||||
await deleteDocument(config.collections.termineConfig, funktion.$id)
|
||||
return true
|
||||
} catch (err) {
|
||||
if (err?.status === 404 && !fehlendeCollection(err)) return false // schon weg
|
||||
throw err
|
||||
}
|
||||
}),
|
||||
false,
|
||||
)
|
||||
}
|
||||
if (geloescht > 0 || einstellungWeg) erg.funktionen += 1
|
||||
}
|
||||
return erg
|
||||
}
|
||||
@@ -386,6 +428,25 @@ export async function taeglicherJob(now) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Zeile fuers Log nach dem Tagesjob: nur Zahlen, keine personenbezogenen Daten und keine Buchungs- oder Funktions-IDs
|
||||
* (Spur der unwiderruflichen Loeschungen). null, wenn der Job nicht gelaufen ist (Modus off, uebersprungen).
|
||||
* @param {object} ergebnis Rueckgabe von taeglicherJob
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function tagesbericht(ergebnis) {
|
||||
if (!ergebnis || ergebnis.aus || ergebnis.uebersprungen) return null
|
||||
const l = ergebnis.loeschfristen
|
||||
const a = ergebnis.abgeschaltet
|
||||
const gesamt = Object.values(l).reduce((summe, n) => summe + n, 0) + a.buchungen
|
||||
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`
|
||||
)
|
||||
}
|
||||
|
||||
/** Nur fuer Tests: Ueberlappungsmerker und Warnungs-Zeitpunkte zuruecksetzen. */
|
||||
export function resetJobsForTests() {
|
||||
laufend.clear()
|
||||
|
||||
Reference in new issue
Block a user