Terminplaner: Zellen nur unter der Ressourcensperre freigeben, gemeinsame Hilfsfunktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e281db1b2b
commit
9ad8515d60
6 files changed
+284
-71
No files matched your search
+161
-1
@@ -19,7 +19,7 @@ useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { deleteDocument, listDocuments } = await import('../server/services/appwriteAdmin.js')
|
||||
const { withLocks } = await import('../server/services/keyedLock.js')
|
||||
const { zellenFuer, zellenId, belegeZellen, gibZellenFrei, verwaisteZellen } = await import('../server/services/termine/zellen.js')
|
||||
const { zellenFuer, zellenId, belegeZellen, gibZellenFrei, verwaisteZellen, raeumeVerwaisteZellen } = await import('../server/services/termine/zellen.js')
|
||||
const { neuesToken, hashToken, verwaltungsToken, buchungsNummer, formToken, pruefeFormToken } = await import(
|
||||
'../server/services/termine/token.js'
|
||||
)
|
||||
@@ -73,6 +73,30 @@ async function belege(id, von, bis, over = {}) {
|
||||
const claimsOf = (bookingId) => [...aw.docs('termineClaims').values()].filter((doc) => doc.bookingId === bookingId)
|
||||
const zeitenOf = (bookingId) => claimsOf(bookingId).map((doc) => hhmm(doc.zelle)).sort()
|
||||
const alleClaims = () => aw.docs('termineClaims').size
|
||||
const schlafe = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
/** Die ersten n DELETE-Anfragen an Appwrite brauchen ms laenger (ein langsamer Aufraeum-Lauf). */
|
||||
async function mitVerzoegertemLoeschen(n, ms, fn) {
|
||||
const echt = globalThis.fetch
|
||||
let gezaehlt = 0
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
if (optionen?.method === 'DELETE' && gezaehlt++ < n) await schlafe(ms)
|
||||
return echt(url, optionen)
|
||||
}
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
globalThis.fetch = echt
|
||||
}
|
||||
}
|
||||
|
||||
/** Haelt die Ressourcensperre, bis freigeben() gerufen wird. */
|
||||
function haltePerSperre(featureId = F, ressourceId = R) {
|
||||
let freigeben
|
||||
const gehalten = new Promise((resolve) => (freigeben = resolve))
|
||||
const fertig = withLocks([`termine-res:${featureId}:${ressourceId}`], () => gehalten)
|
||||
return { freigeben, fertig }
|
||||
}
|
||||
const vergeben = (err) => err.status === 409 && err.code === 'SLOT_VERGEBEN'
|
||||
|
||||
// ---------------------------------------------------------------- Tokens
|
||||
@@ -525,6 +549,54 @@ test('gibZellenFrei: ein Fehler bleibt nicht unbemerkt, die uebrigen Zellen werd
|
||||
assert.equal(claimsOf('b_x').length, 1)
|
||||
})
|
||||
|
||||
test('spaetes Aufraeumen einer alten Buchung loescht nicht die frisch belegten Zellen einer neuen (verzoegertes DELETE)', async () => {
|
||||
// X: abgelaufene Haltung mit Zellen; W belegt dieselbe Zeit, waehrend der Aufraeum-Lauf von X noch loescht
|
||||
seedBuchung('b_X', { status: 'gehalten', heldUntil: iso(new Date(JETZT.getTime() - 1000)) })
|
||||
await belegeZellen({ featureId: F, ressourceId: R, bookingId: 'b_X', start: T('10:00'), blockEnde: T('10:50'), now: new Date(JETZT.getTime() - 60000) })
|
||||
seedBuchung('b_W')
|
||||
await mitVerzoegertemLoeschen(10, 400, async () => {
|
||||
const lauf = gibZellenFrei('b_X')
|
||||
await schlafe(20)
|
||||
await belegeZellen({ featureId: F, ressourceId: R, bookingId: 'b_W', start: T('10:00'), blockEnde: T('10:50'), now: JETZT })
|
||||
await lauf
|
||||
})
|
||||
assert.equal(claimsOf('b_W').length, 10, 'W behaelt alle Zellen')
|
||||
assert.equal(claimsOf('b_X').length, 0)
|
||||
})
|
||||
|
||||
test('gibZellenFrei loescht nur, was nach dem Warten auf die Sperre noch zur Buchung gehoert', async () => {
|
||||
await belege('b_X', T('10:00'), T('10:30'))
|
||||
seedBuchung('b_W')
|
||||
const sperre = haltePerSperre()
|
||||
const lauf = gibZellenFrei('b_X')
|
||||
await schlafe(100) // der Lauf hat gelesen und wartet auf die Sperre
|
||||
// in der Zwischenzeit gehen die Zellen an W (X abgesagt, W belegt dieselben IDs neu)
|
||||
seedBuchung('b_X', { status: 'abgesagt' })
|
||||
for (const claim of claimsOf('b_X')) aw.seed('termineClaims', { ...claim, bookingId: 'b_W' })
|
||||
sperre.freigeben()
|
||||
await sperre.fertig
|
||||
assert.equal(await lauf, 0)
|
||||
assert.equal(claimsOf('b_W').length, 6)
|
||||
})
|
||||
|
||||
test('gibZellenFrei sperrt jede Ressource der Buchung, auch mehrere zugleich', async () => {
|
||||
await belege('b_X', T('10:00'), T('10:30'))
|
||||
await belegeZellen({ featureId: F, ressourceId: 'r_aaaaaaa2', bookingId: 'b_X', start: T('10:00'), blockEnde: T('10:30'), now: JETZT, ausser: 'b_X' })
|
||||
const sperre = haltePerSperre(F, 'r_aaaaaaa2')
|
||||
let fertig = false
|
||||
const lauf = gibZellenFrei('b_X').then((n) => {
|
||||
fertig = true
|
||||
return n
|
||||
})
|
||||
await schlafe(100)
|
||||
assert.equal(fertig, false, 'wartet auf die Sperre der zweiten Ressource')
|
||||
assert.equal(claimsOf('b_X').length, 12)
|
||||
sperre.freigeben()
|
||||
await sperre.fertig
|
||||
assert.equal(await lauf, 12)
|
||||
assert.equal(claimsOf('b_X').length, 0)
|
||||
})
|
||||
|
||||
test('verwaisteZellen: Buchung fehlt, nicht mehr belegend oder Zelle laenger als einen Tag vorbei', async () => {
|
||||
const claim = (id, bookingId, bis, extra = {}) =>
|
||||
aw.seed('termineClaims', {
|
||||
@@ -582,6 +654,94 @@ test('verwaisteZellen liest alle Zellen (mehr als eine Seite) und prueft ihre Bu
|
||||
assert.deepEqual([...ids].sort(), erwartet.sort())
|
||||
})
|
||||
|
||||
/** Zellen mit eigener ID in der Datenbank anlegen (ohne belegeZellen, damit auch ungueltige Altlasten entstehen). */
|
||||
function seedZelle(id, bookingId, bis, extra = {}) {
|
||||
aw.seed('termineClaims', {
|
||||
$id: id,
|
||||
featureId: F,
|
||||
ressourceId: R,
|
||||
bookingId,
|
||||
zelle: iso(new Date(new Date(bis).getTime() - 5 * MIN)),
|
||||
bis: iso(bis),
|
||||
...extra,
|
||||
})
|
||||
}
|
||||
|
||||
test('raeumeVerwaisteZellen loescht genau die verwaisten Zellen und zaehlt sie', async () => {
|
||||
const bald = new Date(JETZT.getTime() + 2 * TAG)
|
||||
seedBuchung('b_ok')
|
||||
seedBuchung('b_abgesagt', { status: 'abgesagt' })
|
||||
seedBuchung('b_haltung_alt', { status: 'gehalten', heldUntil: iso(new Date(JETZT.getTime() - MIN)) })
|
||||
seedBuchung('b_alt')
|
||||
seedZelle('c_ok', 'b_ok', bald)
|
||||
seedZelle('c_abgesagt', 'b_abgesagt', bald, { ressourceId: 'r_aaaaaaa2' })
|
||||
seedZelle('c_haltung_alt', 'b_haltung_alt', bald)
|
||||
seedZelle('c_fehlt', 'b_gibt_es_nicht', bald, { featureId: 'f_feature02' })
|
||||
seedZelle('c_alt', 'b_alt', new Date(JETZT.getTime() - TAG - MIN))
|
||||
assert.equal(await raeumeVerwaisteZellen(JETZT), 4)
|
||||
assert.deepEqual([...aw.docs('termineClaims').keys()], ['c_ok'])
|
||||
assert.equal(await raeumeVerwaisteZellen(JETZT), 0)
|
||||
await assert.rejects(raeumeVerwaisteZellen(undefined), TypeError)
|
||||
})
|
||||
|
||||
test('raeumeVerwaisteZellen raeumt mehr als einen Block (260 Zellen auf zwei Ressourcen)', async () => {
|
||||
for (let i = 0; i < 260; i += 1) {
|
||||
seedZelle(`c_viele${String(i).padStart(3, '0')}`, `b_nie${i}`, T('23:00'), { ressourceId: i % 2 ? 'r_aaaaaaa2' : R })
|
||||
}
|
||||
seedBuchung('b_ok')
|
||||
seedZelle('c_ok', 'b_ok', T('23:00'))
|
||||
assert.equal(await raeumeVerwaisteZellen(JETZT), 260)
|
||||
assert.deepEqual([...aw.docs('termineClaims').keys()], ['c_ok'])
|
||||
})
|
||||
|
||||
test('raeumeVerwaisteZellen: ein verzoegertes DELETE trifft nicht die frisch belegten Zellen einer neuen Buchung', async () => {
|
||||
seedBuchung('b_X', { status: 'abgesagt' })
|
||||
await belegeZellen({ featureId: F, ressourceId: R, bookingId: 'b_X', start: T('10:00'), blockEnde: T('10:50'), now: JETZT })
|
||||
seedBuchung('b_W')
|
||||
await mitVerzoegertemLoeschen(10, 400, async () => {
|
||||
const lauf = raeumeVerwaisteZellen(JETZT)
|
||||
await schlafe(20)
|
||||
await belegeZellen({ featureId: F, ressourceId: R, bookingId: 'b_W', start: T('10:00'), blockEnde: T('10:50'), now: JETZT })
|
||||
await lauf
|
||||
})
|
||||
assert.equal(claimsOf('b_W').length, 10, 'W behaelt alle Zellen')
|
||||
assert.equal(claimsOf('b_X').length, 0)
|
||||
})
|
||||
|
||||
test('raeumeVerwaisteZellen prueft nach dem Warten auf die Sperre erneut: wieder gueltige oder neu vergebene Zellen bleiben', async () => {
|
||||
const bald = new Date(JETZT.getTime() + 2 * TAG)
|
||||
seedBuchung('b_X1', { status: 'abgesagt' })
|
||||
seedBuchung('b_X2', { status: 'abgesagt' })
|
||||
seedBuchung('b_X3', { status: 'abgesagt' })
|
||||
seedBuchung('b_W')
|
||||
seedZelle('c_x1', 'b_X1', bald)
|
||||
seedZelle('c_x2', 'b_X2', bald)
|
||||
seedZelle('c_x3', 'b_X3', bald)
|
||||
seedZelle('c_x4', 'b_X1', bald)
|
||||
const sperre = haltePerSperre()
|
||||
const lauf = raeumeVerwaisteZellen(JETZT)
|
||||
await schlafe(100) // der Lauf hat die Kandidaten gelesen und wartet auf die Sperre
|
||||
seedBuchung('b_X1', { status: 'bestaetigt' }) // wieder gueltig
|
||||
aw.seed('termineClaims', { ...aw.docs('termineClaims').get('c_x2'), bookingId: 'b_W' }) // neu vergeben
|
||||
await deleteDocument('termineClaims', 'c_x3') // schon weg
|
||||
aw.seed('termineClaims', { ...aw.docs('termineClaims').get('c_x4'), bookingId: 'b_neu_ohne_buchung' }) // neu vergeben, Buchung folgt erst noch
|
||||
sperre.freigeben()
|
||||
await sperre.fertig
|
||||
assert.equal(await lauf, 0)
|
||||
assert.deepEqual([...aw.docs('termineClaims').keys()].sort(), ['c_x1', 'c_x2', 'c_x4'])
|
||||
assert.equal(aw.docs('termineClaims').get('c_x2').bookingId, 'b_W')
|
||||
assert.equal(aw.docs('termineClaims').get('c_x4').bookingId, 'b_neu_ohne_buchung')
|
||||
})
|
||||
|
||||
test('raeumeVerwaisteZellen: ein Fehler in einem Block stoppt die uebrigen nicht und wird am Ende geworfen', async () => {
|
||||
seedZelle('c_a1', 'b_nie1', T('23:00'))
|
||||
seedZelle('c_b1', 'b_nie2', T('23:00'), { ressourceId: 'r_aaaaaaa2' })
|
||||
aw.failNext({ collection: 'termineClaims', method: 'DELETE', status: 503 })
|
||||
await assert.rejects(raeumeVerwaisteZellen(JETZT), (err) => err.status === 500)
|
||||
assert.equal(aw.docs('termineClaims').size, 1, 'die andere Ressource ist trotzdem geraeumt')
|
||||
assert.equal(await raeumeVerwaisteZellen(JETZT), 1)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Buchungsspeicher
|
||||
|
||||
test('legeAn, hole, aendere: Anlegen mit fester ID, Lesen (fehlt => null), Aendern setzt updatedAt', async () => {
|
||||
|
||||
Reference in new issue
Block a user