Terminplaner: Minutenjob in Verfall und Mailversand getrennt geschuetzt
fuehreAus nimmt { teil } und sperrt je job:teil; minutenJob startet Verfall
und Mailversand (Warteschlange plus Betriebspruefung) gemeinsam mit
Promise.all. Haengt ein Teil, laeuft der andere im naechsten Takt weiter
(verfallUebersprungen bzw. mailUebersprungen); uebersprungen gilt nur, wenn
beide Teile uebersprungen werden. Die Ueberlappungswarnung nennt den Teil,
das Fehlerlog behaelt minutenJob.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aaed273b62
commit
e2f50407a0
2 files changed
+190
-40
No files matched your search
+132
-19
@@ -264,6 +264,54 @@ async function mitEinwurf(passt, dazwischen, fn) {
|
||||
}
|
||||
const listeVonBuchungen = (muster) => (url, methode) =>
|
||||
methode === 'GET' && /\/collections\/termineBookings\/documents(\?|$)/.test(url) && muster.test(url)
|
||||
const listeVonOutbox = (url, methode) => methode === 'GET' && /\/collections\/termineOutbox\/documents(\?|$)/.test(url)
|
||||
|
||||
/**
|
||||
* Haengende Schritte: haelt je Muster (url, methode) => boolean die erste Anfrage an, auf die es zutrifft, bis freigeben() alle
|
||||
* loslaesst. warteBisRuhig() wartet, bis keine andere Anfrage mehr laeuft und 50 ms lang keine neue kam (die uebrigen Schritte
|
||||
* des Takts sind dann durch). ende() stellt fetch wieder her.
|
||||
*/
|
||||
function mitHaengern(...muster) {
|
||||
const echt = globalThis.fetch
|
||||
const tore = muster.map(() => {
|
||||
const tor = { gehalten: false }
|
||||
tor.warte = new Promise((resolve) => (tor.los = resolve))
|
||||
return tor
|
||||
})
|
||||
let offen = 0
|
||||
let anfragen = 0
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
anfragen += 1
|
||||
const text = decodeURIComponent(String(url))
|
||||
const methode = optionen?.method ?? 'GET'
|
||||
const i = muster.findIndex((passt, k) => !tore[k].gehalten && passt(text, methode))
|
||||
if (i >= 0) {
|
||||
tore[i].gehalten = true
|
||||
await tore[i].warte
|
||||
return echt(url, optionen)
|
||||
}
|
||||
offen += 1
|
||||
try {
|
||||
return await echt(url, optionen)
|
||||
} finally {
|
||||
offen -= 1
|
||||
}
|
||||
}
|
||||
return {
|
||||
freigeben: () => tore.forEach((tor) => tor.los()),
|
||||
gehalten: () => tore.every((tor) => tor.gehalten),
|
||||
async warteBisRuhig() {
|
||||
for (let ruhig = 0; ruhig < 5; ) {
|
||||
const stand = anfragen
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
ruhig = offen === 0 && stand === anfragen ? ruhig + 1 : 0
|
||||
}
|
||||
},
|
||||
ende: () => {
|
||||
globalThis.fetch = echt
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Alle Lese-Anfragen an die Collection scheitern wie in Appwrite mit 404 collection_not_found. */
|
||||
async function mitFehlenderCollection(collection, fn) {
|
||||
@@ -1974,46 +2022,111 @@ test('fehlende Collection der Warteschlange: der Minutenjob verfällt trotzdem H
|
||||
assert.equal(warnLog().length, 1)
|
||||
})
|
||||
|
||||
test('überlappende Läufe: ein langsamer Lauf wird vom nächsten Takt übersprungen (einmal im Log), danach geht es normal weiter', async () => {
|
||||
test('überlappende Läufe: hängen beide Teile des Minutenjobs, werden die nächsten Takte übersprungen (je Teil einmal im Log), danach geht es normal weiter', async () => {
|
||||
await legeBuchung('b_vorbei', { status: 'gehalten', heldUntil: iso(plus(JETZT, -MIN)) })
|
||||
const echt = globalThis.fetch
|
||||
let freigeben
|
||||
const tor = new Promise((resolve) => (freigeben = resolve))
|
||||
let erste = true
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
if (erste) {
|
||||
erste = false
|
||||
await tor
|
||||
}
|
||||
return echt(url, optionen)
|
||||
}
|
||||
const haenger = mitHaengern(listeVonBuchungen(/heldUntil/), listeVonOutbox)
|
||||
try {
|
||||
const langsam = minutenJob(JETZT)
|
||||
await haenger.warteBisRuhig()
|
||||
assert.ok(haenger.gehalten(), 'Verfall und Mailversand hängen')
|
||||
assert.deepEqual(await minutenJob(plus(JETZT, MIN)), { uebersprungen: true })
|
||||
assert.deepEqual(await minutenJob(plus(JETZT, 2 * MIN)), { uebersprungen: true })
|
||||
assert.equal(warnLog().filter((zeile) => /minutenJob/.test(zeile)).length, 1, 'nur einmal gemeldet')
|
||||
assert.equal(warnLog().filter((zeile) => /minutenJob \(Verfall\)/.test(zeile)).length, 1, 'Verfall nur einmal gemeldet')
|
||||
assert.equal(warnLog().filter((zeile) => /minutenJob \(Mailversand\)/.test(zeile)).length, 1, 'Mailversand nur einmal gemeldet')
|
||||
assert.equal(warnLog().filter((zeile) => /minutenJob/.test(zeile)).length, 2, 'sonst keine Warnung')
|
||||
// andere Jobs sind davon nicht betroffen
|
||||
assert.equal((await fuenfMinutenJob(JETZT)).uebersprungen, undefined)
|
||||
freigeben()
|
||||
haenger.freigeben()
|
||||
assert.equal((await langsam).verfallen, 1)
|
||||
} finally {
|
||||
globalThis.fetch = echt
|
||||
haenger.freigeben()
|
||||
haenger.ende()
|
||||
}
|
||||
assert.equal((await minutenJob(plus(JETZT, 3 * MIN))).uebersprungen, undefined, 'danach läuft der Job wieder')
|
||||
assert.deepEqual(await minutenJob(plus(JETZT, 3 * MIN)), { verfallen: 0, faellig: 0, gesendet: 0, fehler: 0 }, 'danach laufen beide Teile wieder')
|
||||
})
|
||||
|
||||
test('ein Job, der mit einem Fehler endet, gibt die Überlappungssperre frei', async () => {
|
||||
test('überlappende Läufe: hängt der Verfall, versendet der nächste Takt trotzdem die Warteschlange (Überlappung einmal im Log)', async () => {
|
||||
await legeBuchung('b_vorbei', { status: 'gehalten', heldUntil: iso(plus(JETZT, -MIN)) })
|
||||
legeEintrag('o_erst')
|
||||
const haenger = mitHaengern(listeVonBuchungen(/heldUntil/))
|
||||
try {
|
||||
const langsam = minutenJob(JETZT)
|
||||
await haenger.warteBisRuhig()
|
||||
assert.ok(haenger.gehalten(), 'der Verfall hängt')
|
||||
// beide Teile starten gemeinsam: die Warteschlange ist abgearbeitet, obwohl der Verfall des ersten Takts noch hängt
|
||||
assert.equal(aw.docs('termineOutbox').get('o_erst').status, 'gesendet')
|
||||
assert.equal(status('b_vorbei'), 'gehalten')
|
||||
|
||||
legeEintrag('o_zweit')
|
||||
const zweiter = await minutenJob(plus(JETZT, MIN))
|
||||
assert.deepEqual(zweiter, { verfallen: 0, faellig: 1, gesendet: 1, fehler: 0, verfallUebersprungen: true })
|
||||
assert.equal(aw.docs('termineOutbox').get('o_zweit').status, 'gesendet')
|
||||
assert.equal(sink.sent.length, 2)
|
||||
const dritter = await minutenJob(plus(JETZT, 2 * MIN))
|
||||
assert.equal(dritter.verfallUebersprungen, true)
|
||||
assert.equal(dritter.uebersprungen, undefined, 'nur ein Teil übersprungen: der Takt gilt nicht als übersprungen')
|
||||
assert.equal(warnLog().filter((zeile) => /minutenJob \(Verfall\)/.test(zeile)).length, 1, 'die Überlappung steht einmal im Log')
|
||||
assert.equal(warnLog().filter((zeile) => /minutenJob/.test(zeile)).length, 1, 'der Mailversand wurde nicht übersprungen')
|
||||
|
||||
haenger.freigeben()
|
||||
assert.deepEqual(await langsam, { verfallen: 1, faellig: 1, gesendet: 1, fehler: 0 })
|
||||
} finally {
|
||||
haenger.freigeben()
|
||||
haenger.ende()
|
||||
}
|
||||
assert.equal(status('b_vorbei'), 'verfallen')
|
||||
assert.deepEqual(await minutenJob(plus(JETZT, 3 * MIN)), { verfallen: 0, faellig: 0, gesendet: 0, fehler: 0 })
|
||||
})
|
||||
|
||||
test('überlappende Läufe: hängt der Mailversand, verfällt im nächsten Takt eine abgelaufene Haltung trotzdem (Überlappung einmal im Log)', async () => {
|
||||
const haenger = mitHaengern(listeVonOutbox)
|
||||
try {
|
||||
const langsam = minutenJob(JETZT)
|
||||
await haenger.warteBisRuhig()
|
||||
assert.ok(haenger.gehalten(), 'der Mailversand hängt')
|
||||
|
||||
await legeBuchung('b_vorbei', { status: 'gehalten', heldUntil: iso(plus(JETZT, MIN)) })
|
||||
assert.ok(claims('b_vorbei').length > 0)
|
||||
const zweiter = await minutenJob(plus(JETZT, 2 * MIN))
|
||||
assert.deepEqual(zweiter, { verfallen: 1, faellig: 0, gesendet: 0, fehler: 0, mailUebersprungen: true })
|
||||
assert.equal(status('b_vorbei'), 'verfallen')
|
||||
assert.equal(claims('b_vorbei').length, 0, 'Zellen frei')
|
||||
const dritter = await minutenJob(plus(JETZT, 3 * MIN))
|
||||
assert.equal(dritter.mailUebersprungen, true)
|
||||
assert.equal(dritter.uebersprungen, undefined)
|
||||
assert.equal(warnLog().filter((zeile) => /minutenJob \(Mailversand\)/.test(zeile)).length, 1, 'die Überlappung steht einmal im Log')
|
||||
assert.equal(warnLog().filter((zeile) => /minutenJob/.test(zeile)).length, 1, 'der Verfall wurde nicht übersprungen')
|
||||
|
||||
haenger.freigeben()
|
||||
assert.deepEqual(await langsam, { verfallen: 0, faellig: 0, gesendet: 0, fehler: 0 })
|
||||
} finally {
|
||||
haenger.freigeben()
|
||||
haenger.ende()
|
||||
}
|
||||
assert.deepEqual(await minutenJob(plus(JETZT, 4 * MIN)), { verfallen: 0, faellig: 0, gesendet: 0, fehler: 0 })
|
||||
})
|
||||
|
||||
test('ein Job, der mit einem Fehler endet, gibt die Überlappungssperren frei', async () => {
|
||||
const echt = globalThis.fetch
|
||||
globalThis.fetch = async () => {
|
||||
throw new TypeError('fetch failed')
|
||||
}
|
||||
try {
|
||||
const erg = await minutenJob(JETZT)
|
||||
assert.equal(erg.fehler, 2, 'Verfall und Warteschlange scheitern, der Job wirft nicht')
|
||||
assert.equal(erg.fehler, 2, 'Verfall und Warteschlange scheitern (je Teil ein Fehler), der Job wirft nicht')
|
||||
assert.deepEqual(
|
||||
fehlerLog()
|
||||
.map((zeile) => /^\[termine-jobs\] minutenJob: (Verfall|Warteschlange):/.exec(zeile)?.[1])
|
||||
.filter(Boolean)
|
||||
.sort(),
|
||||
['Verfall', 'Warteschlange'],
|
||||
'das Fehlerlog behält minutenJob als Jobnamen',
|
||||
)
|
||||
} finally {
|
||||
globalThis.fetch = echt
|
||||
}
|
||||
assert.equal((await minutenJob(JETZT)).uebersprungen, undefined)
|
||||
assert.deepEqual(await minutenJob(JETZT), { verfallen: 0, faellig: 0, gesendet: 0, fehler: 0 }, 'beide Sperren sind frei')
|
||||
assert.equal(warnLog().filter((zeile) => /nicht fertig/.test(zeile)).length, 0)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Einbindung
|
||||
|
||||
Reference in new issue
Block a user