Workers P4 Endprüfung W1: 502/504 beim Warten heißen bei reply, approve-preview und start-build „Kann gesendet sein“
Die Unsicherheits-Meldung (D14) galt nur für status 0. Eine Fehlerseite des Proxys (502, 504) zeigte „Der Hustler-Dienst ist gerade nicht erreichbar.“, obwohl der Dienst weiterarbeitet und die Mail schon hinausgegangen sein kann. workersApi.js exportiert UNCERTAIN_STATUS = [0, 502, 504]; actionErrorText prüft ihn (503, 429, 4xx und 500 bleiben unverändert, ebenso ein Satz des Dienstes unter 502). ACTION_TIMEOUT_MS steigt auf 65 s, damit der Client nicht gleichauf mit dem proxy_read_timeout (60 s) läuft und ein 504 zuerst ankommt. Tests über createWorkersApi mit Fehlerseiten für alle acht Aktionen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5138efd2bf
commit
1536a3bbc1
4 files changed
+68
-31
No files matched your search
@@ -13,7 +13,9 @@ export const INVALID_ID_TEXT = 'Ungültige Kennung.'
|
|||||||
export const REQUEST_TIMEOUT_MS = 20000
|
export const REQUEST_TIMEOUT_MS = 20000
|
||||||
// Zeitgrenze für Pult-Aktionen, die im Dienst länger dauern (Mail senden, Freigabe einreihen, Bau anlegen): nach 20 s als „nicht erreichbar“
|
// Zeitgrenze für Pult-Aktionen, die im Dienst länger dauern (Mail senden, Freigabe einreihen, Bau anlegen): nach 20 s als „nicht erreichbar“
|
||||||
// abzubrechen, obwohl die Mail gerade hinausgeht, ließe den Menschen ein zweites Mal klicken. Alle anderen Abrufe behalten die kurze Frist.
|
// abzubrechen, obwohl die Mail gerade hinausgeht, ließe den Menschen ein zweites Mal klicken. Alle anderen Abrufe behalten die kurze Frist.
|
||||||
export const ACTION_TIMEOUT_MS = 60000
|
// 65 s liegen bewusst über dem proxy_read_timeout des Proxys (nginx, 60 s): der Client läuft nicht mit nginx um die Wette, eine Zeitüberschreitung
|
||||||
|
// im Proxy kommt als 504 zuerst an (UNCERTAIN_STATUS), erst danach greift die Frist des Clients.
|
||||||
|
export const ACTION_TIMEOUT_MS = 65000
|
||||||
// Nach einer 401-Folge (frisches JWT wird ebenfalls abgelehnt oder Appwrite lehnt createJWT ab) wird so lange kein frisches JWT mehr
|
// Nach einer 401-Folge (frisches JWT wird ebenfalls abgelehnt oder Appwrite lehnt createJWT ab) wird so lange kein frisches JWT mehr
|
||||||
// erzwungen, und lehnt Appwrite createJWT ab (HTTP-Code ab 400, z. B. 429; kein Übertragungsfehler), wird so lange gar kein JWT mehr geholt: Appwrite begrenzt
|
// erzwungen, und lehnt Appwrite createJWT ab (HTTP-Code ab 400, z. B. 429; kein Übertragungsfehler), wird so lange gar kein JWT mehr geholt: Appwrite begrenzt
|
||||||
// createJWT je Nutzer, und die Schleifen (5 s / 15 s) würden das Limit binnen Minuten aufbrauchen und damit auch Serverpuls,
|
// createJWT je Nutzer, und die Schleifen (5 s / 15 s) würden das Limit binnen Minuten aufbrauchen und damit auch Serverpuls,
|
||||||
@@ -26,6 +28,10 @@ export const PULT_STATUS = Object.freeze(['offen', 'in_arbeit', 'gesendet', 'ver
|
|||||||
const PULT_ACTIONS = Object.freeze(['claim', 'sent', 'discard', 'done', 'reply', 'approve-preview', 'paste', 'start-build'])
|
const PULT_ACTIONS = Object.freeze(['claim', 'sent', 'discard', 'done', 'reply', 'approve-preview', 'paste', 'start-build'])
|
||||||
// Die Aktionen mit der langen Frist (ACTION_TIMEOUT_MS); bei einer Zeitüberschreitung ist offen, ob der Dienst sie ausgeführt hat
|
// Die Aktionen mit der langen Frist (ACTION_TIMEOUT_MS); bei einer Zeitüberschreitung ist offen, ob der Dienst sie ausgeführt hat
|
||||||
export const SLOW_PULT_ACTIONS = Object.freeze(['reply', 'approve-preview', 'start-build'])
|
export const SLOW_PULT_ACTIONS = Object.freeze(['reply', 'approve-preview', 'start-build'])
|
||||||
|
// Status eines Fehlers, bei dem offen bleibt, ob der Dienst die Aktion trotzdem ausgeführt hat (D14): 0 = der Client hat abgebrochen oder die
|
||||||
|
// Verbindung riss, 502 und 504 = der Proxy hat beim Warten aufgegeben (der Dienst arbeitet weiter, die Mail kann hinausgegangen sein). 503 fehlt mit
|
||||||
|
// Absicht: dort gibt es keinen Upstream, die Anfrage hat den Dienst nie erreicht. Dasselbe gilt für 429 (Drosselung) und die Fehler 4xx/500.
|
||||||
|
export const UNCERTAIN_STATUS = Object.freeze([0, 502, 504])
|
||||||
|
|
||||||
const BASE = '/api/workers'
|
const BASE = '/api/workers'
|
||||||
const EVENTS_LIMIT = 200
|
const EVENTS_LIMIT = 200
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// Plattform-Antworten schickt der Mensch selbst ab (Plattform) und markiert sie danach als gesendet; Mail-Antworten und die Freigabe
|
// Plattform-Antworten schickt der Mensch selbst ab (Plattform) und markiert sie danach als gesendet; Mail-Antworten und die Freigabe
|
||||||
// einer Vorschau gehen über den Dienst (api.pultAction), der sie durch das Sende-Tor führt.
|
// einer Vorschau gehen über den Dienst (api.pultAction), der sie durch das Sende-Tor führt.
|
||||||
|
|
||||||
import { SLOW_PULT_ACTIONS } from './workersApi'
|
import { DOWN_TEXT, SLOW_PULT_ACTIONS, UNCERTAIN_STATUS } from './workersApi'
|
||||||
import { roleOf } from './workersRoster'
|
import { roleOf } from './workersRoster'
|
||||||
|
|
||||||
const MINUTE_MS = 60 * 1000
|
const MINUTE_MS = 60 * 1000
|
||||||
@@ -288,11 +288,14 @@ export function validateReplyText(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Meldung der Zeile bei einer gescheiterten Pult-Aktion: der Satz des Dienstes. Eine Zeitüberschreitung oder ein Netzfehler (status 0) bei
|
* Meldung der Zeile bei einer gescheiterten Pult-Aktion: der Satz des Dienstes. Eine Zeitüberschreitung oder ein Netzfehler (status 0) und die
|
||||||
* reply, approve-preview und start-build heißt nicht „nicht gesendet“: der Dienst kann die Aktion noch ausgeführt haben (D14).
|
* Fehlerseite des Proxys beim Warten (502, 504) bei reply, approve-preview und start-build heißen nicht „nicht gesendet“: der Dienst kann die
|
||||||
|
* Aktion noch ausgeführt haben (D14, UNCERTAIN_STATUS). Bei 502/504 gilt das nur für die Fehlerseite des Proxys (Meldung DOWN_TEXT); ein Satz
|
||||||
|
* des Dienstes unter demselben Status (502 „Anmeldedienst nicht erreichbar.“ kommt vor der Aktion) bleibt stehen.
|
||||||
*/
|
*/
|
||||||
export function actionErrorText(err, action) {
|
export function actionErrorText(err, action) {
|
||||||
if (SLOW_PULT_ACTIONS.includes(action) && err?.status === 0) return UNCERTAIN_TEXT
|
const status = err?.status
|
||||||
|
if (SLOW_PULT_ACTIONS.includes(action) && UNCERTAIN_STATUS.includes(status) && (status === 0 || err.message === DOWN_TEXT)) return UNCERTAIN_TEXT
|
||||||
return typeof err?.message === 'string' && err.message !== '' ? err.message : ACTION_FAILED
|
return typeof err?.message === 'string' && err.message !== '' ? err.message : ACTION_FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import assert from 'node:assert/strict'
|
|||||||
import { loadModule } from './load.mjs'
|
import { loadModule } from './load.mjs'
|
||||||
|
|
||||||
// API-Client der Hustler-Seite: URLs, Methoden, Header, deutsche Fehlertexte, JWT-Wiederholung, Pult-Prüfungen
|
// API-Client der Hustler-Seite: URLs, Methoden, Header, deutsche Fehlertexte, JWT-Wiederholung, Pult-Prüfungen
|
||||||
const { createWorkersApi, REQUEST_TIMEOUT_MS, AUTH_COOLDOWN_MS, ACTION_TIMEOUT_MS, SLOW_PULT_ACTIONS } = await loadModule('src/lib/workersApi.js')
|
const { createWorkersApi, REQUEST_TIMEOUT_MS, AUTH_COOLDOWN_MS, ACTION_TIMEOUT_MS, SLOW_PULT_ACTIONS, UNCERTAIN_STATUS } = await loadModule('src/lib/workersApi.js')
|
||||||
|
const { actionErrorText, UNCERTAIN_TEXT } = await loadModule('src/lib/workersPult.js')
|
||||||
const { createJwtCache } = await loadModule('src/lib/serverpulsAuth.js')
|
const { createJwtCache } = await loadModule('src/lib/serverpulsAuth.js')
|
||||||
|
|
||||||
const jwts = []
|
const jwts = []
|
||||||
@@ -194,40 +195,54 @@ test('Zeitgrenze: rechtzeitige Antworten bleiben unberührt, ein späterer Abruf
|
|||||||
assert.deepEqual(await quick.state(), {})
|
assert.deepEqual(await quick.state(), {})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Zeitgrenze: Standardwerte 20 s und 2 min, für langsame Pult-Aktionen 60 s', () => {
|
test('Zeitgrenze: Standardwerte 20 s und 2 min, für langsame Pult-Aktionen 65 s (nach der Frist des Proxys von 60 s)', () => {
|
||||||
assert.equal(REQUEST_TIMEOUT_MS, 20000)
|
assert.equal(REQUEST_TIMEOUT_MS, 20000)
|
||||||
assert.equal(AUTH_COOLDOWN_MS, 120000)
|
assert.equal(AUTH_COOLDOWN_MS, 120000)
|
||||||
assert.equal(ACTION_TIMEOUT_MS, 60000)
|
// nginx bricht nach proxy_read_timeout 60 s mit 504 ab; die Frist des Clients liegt darüber, damit nicht von der Uhr abhängt, welche Meldung erscheint
|
||||||
|
assert.equal(ACTION_TIMEOUT_MS, 65000)
|
||||||
assert.deepEqual(SLOW_PULT_ACTIONS, ['reply', 'approve-preview', 'start-build'])
|
assert.deepEqual(SLOW_PULT_ACTIONS, ['reply', 'approve-preview', 'start-build'])
|
||||||
assert.ok(Object.isFrozen(SLOW_PULT_ACTIONS))
|
assert.ok(Object.isFrozen(SLOW_PULT_ACTIONS))
|
||||||
|
// Status, bei dem offen bleibt, ob der Dienst die Aktion ausgeführt hat: Abbruch des Clients (0) und Fehlerseiten des Proxys beim Warten (502, 504)
|
||||||
|
assert.deepEqual(UNCERTAIN_STATUS, [0, 502, 504])
|
||||||
|
assert.ok(Object.isFrozen(UNCERTAIN_STATUS))
|
||||||
})
|
})
|
||||||
|
|
||||||
// P4-T32 (D14): Antworten, Freigeben und Bauen laufen im Dienst länger (SMTP, Warteschlange); bricht der Client nach 20 s ab, sähe der Mensch einen Fehler,
|
// Endprüfung P4 (W1, D14): eine Fehlerseite des Proxys (502/504) bei reply, approve-preview und start-build heißt nicht „nicht erreichbar“:
|
||||||
// obwohl die Mail hinausgeht. Nur diese drei Aktionen bekommen die lange Frist, alles andere bleibt bei der kurzen.
|
// der Dienst arbeitet weiter, wenn nginx aufgibt, die Mail kann schon hinausgegangen sein. Der Client selbst (read) bleibt bei DOWN_TEXT
|
||||||
test('Zeitgrenze (D14): reply, approve-preview und start-build warten länger als die übrigen Pult-Aktionen', async () => {
|
// und dem echten Status; die Aktions-Meldung (actionErrorText) entscheidet nach Aktion und Status.
|
||||||
const slowReply = (ms) => async () => {
|
test('Proxy-Fehlerseite (D14, W1): 502 und 504 bei reply, approve-preview und start-build heißen „Kann gesendet sein“, bei den übrigen Aktionen „nicht erreichbar“', async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, ms))
|
const outcome = async (action, status) => {
|
||||||
return { ok: true, status: 200, json: async () => ({ item: { $id: 'p1' } }) }
|
const api = createWorkersApi({ getJwt, fetchImpl: fakeFetch([{ status, body: undefined }]) }) // HTML-Fehlerseite des Proxys, kein JSON
|
||||||
|
const err = await api.pultAction('p1', action, action === 'reply' || action === 'paste' ? { text: 'x' } : undefined).then(() => null, (e) => e)
|
||||||
|
assert.equal(err.status, status, `${action} ${status}`)
|
||||||
|
if ([502, 503, 504].includes(status)) assert.equal(err.message, DOWN, `${action} ${status}: der Client meldet weiter „nicht erreichbar“`)
|
||||||
|
return actionErrorText(err, action)
|
||||||
}
|
}
|
||||||
// kurze Frist 30 ms, lange Frist 400 ms, Antwort nach 120 ms
|
|
||||||
for (const action of ['reply', 'approve-preview', 'start-build']) {
|
for (const action of ['reply', 'approve-preview', 'start-build']) {
|
||||||
const api = createWorkersApi({ getJwt, fetchImpl: slowReply(120), timeoutMs: 30, actionTimeoutMs: 400 })
|
for (const status of [502, 504]) assert.equal(await outcome(action, status), UNCERTAIN_TEXT, `${action} ${status}`)
|
||||||
assert.deepEqual(await api.pultAction('p1', action, action === 'reply' ? { text: 'x' } : undefined), { item: { $id: 'p1' } }, action)
|
// Abbruch des Clients (status 0) wie bisher
|
||||||
|
const aborted = createWorkersApi({ getJwt, fetchImpl: fakeFetch([new TypeError('fetch failed')]) })
|
||||||
|
const err = await aborted.pultAction('p1', action, action === 'reply' ? { text: 'x' } : undefined).then(() => null, (e) => e)
|
||||||
|
assert.equal(actionErrorText(err, action), UNCERTAIN_TEXT, `${action} 0`)
|
||||||
}
|
}
|
||||||
for (const action of ['claim', 'sent', 'discard', 'done', 'paste']) {
|
// 503 (kein Upstream, die Anfrage hat den Dienst nie erreicht) bleibt „nicht erreichbar“, ebenso Drosselung, Fehler des Dienstes und alles Übrige
|
||||||
const api = createWorkersApi({ getJwt, fetchImpl: slowReply(120), timeoutMs: 30, actionTimeoutMs: 400 })
|
for (const action of ['reply', 'approve-preview', 'start-build']) {
|
||||||
await assert.rejects(api.pultAction('p1', action, action === 'paste' ? { text: 'x' } : undefined), { message: DOWN, status: 0 }, action)
|
assert.equal(await outcome(action, 503), DOWN, `${action} 503`)
|
||||||
|
assert.equal(await outcome(action, 500), 'Der Hustler-Dienst antwortet nicht richtig.', `${action} 500 ohne JSON`)
|
||||||
|
assert.equal(await outcome(action, 429), 'Der Hustler-Dienst antwortet nicht richtig.', `${action} 429 ohne JSON`)
|
||||||
|
}
|
||||||
|
// die übrigen Pult-Aktionen: ein 502 bleibt „nicht erreichbar“ (der Dienst führt sie schnell aus, es steht kein Versand dahinter)
|
||||||
|
for (const action of ['paste', 'claim', 'sent', 'discard', 'done']) {
|
||||||
|
assert.equal(await outcome(action, 502), DOWN, `${action} 502`)
|
||||||
|
assert.equal(await outcome(action, 504), DOWN, `${action} 504`)
|
||||||
|
}
|
||||||
|
// ein echter Satz des Dienstes (JSON mit error) bleibt auch bei einer langsamen Aktion und bei 502 unverändert: der Dienst selbst antwortet mit 502
|
||||||
|
// „Anmeldedienst nicht erreichbar.“, wenn die Anmeldung scheitert, und das geschieht vor der Aktion, es wurde also nichts gesendet
|
||||||
|
for (const action of ['reply', 'approve-preview', 'start-build']) {
|
||||||
|
const api = createWorkersApi({ getJwt, fetchImpl: fakeFetch([{ status: 502, body: { error: 'Anmeldedienst nicht erreichbar.' } }]) })
|
||||||
|
const err = await api.pultAction('p1', action, action === 'reply' ? { text: 'x' } : undefined).then(() => null, (e) => e)
|
||||||
|
assert.equal(actionErrorText(err, action), 'Anmeldedienst nicht erreichbar.', action)
|
||||||
}
|
}
|
||||||
// Lesen (Liste, Zustand) behält die kurze Frist
|
|
||||||
await assert.rejects(createWorkersApi({ getJwt, fetchImpl: slowReply(120), timeoutMs: 30, actionTimeoutMs: 400 }).pult(), { message: DOWN, status: 0 })
|
|
||||||
// auch die lange Frist endet: ein hängender Abruf meldet nach ihr die Dienst-Meldung (status 0) und bricht ab
|
|
||||||
const signals = []
|
|
||||||
const hang = (url, init) => { signals.push(init.signal); return new Promise(() => {}) }
|
|
||||||
const api = createWorkersApi({ getJwt, fetchImpl: hang, timeoutMs: 20, actionTimeoutMs: 150 })
|
|
||||||
const t0 = Date.now()
|
|
||||||
await assert.rejects(api.pultAction('p1', 'reply', { text: 'x' }), { message: DOWN, status: 0 })
|
|
||||||
assert.ok(Date.now() - t0 >= 120, 'wartet die lange Frist ab')
|
|
||||||
assert.equal(signals[0].aborted, true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('401-Folge: danach kein frisches JWT mehr, bis die Sperre abgelaufen ist (höchstens ein createJWT je Sperre)', async () => {
|
test('401-Folge: danach kein frisches JWT mehr, bis die Sperre abgelaufen ist (höchstens ein createJWT je Sperre)', async () => {
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ test('rowModel canApprovePreview und previewHref: nur Art vorschau_freigabe, gle
|
|||||||
'https://www.project.webklar.com/AAA/', 'https://project.webklar.com/AAA/', '//entwurf.project.webklar.com/AAA/', '', null, undefined, 7]) {
|
'https://www.project.webklar.com/AAA/', 'https://project.webklar.com/AAA/', '//entwurf.project.webklar.com/AAA/', '', null, undefined, 7]) {
|
||||||
m = rowModel(freigabe({ sourceUrl: bad }), T32_CTX)
|
m = rowModel(freigabe({ sourceUrl: bad }), T32_CTX)
|
||||||
assert.equal(m.previewHref, null, String(bad))
|
assert.equal(m.previewHref, null, String(bad))
|
||||||
assert.equal(m.canApprovePreview, true, String(bad)) // der Knopf bleibt, die Adresse fehlt nur; der Dienst prüft die Freigabe selbst
|
assert.equal(m.canApprovePreview, true, String(bad)) // das Modell bleibt bei true; die Zeile sperrt den Knopf ohne gültige Adresse (Endprüfung W5, workersPultUi.test.mjs)
|
||||||
}
|
}
|
||||||
assert.equal(rowModel(freigabe({ sourceUrl: 'HTTPS://ENTWURF.PROJECT.WEBKLAR.COM/AAA/' }), T32_CTX).previewHref, PREVIEW_URL) // vom Browser normalisiert
|
assert.equal(rowModel(freigabe({ sourceUrl: 'HTTPS://ENTWURF.PROJECT.WEBKLAR.COM/AAA/' }), T32_CTX).previewHref, PREVIEW_URL) // vom Browser normalisiert
|
||||||
// abgeschlossen bleibt die Adresse im Modell (die Zeile braucht sie dort nicht)
|
// abgeschlossen bleibt die Adresse im Modell (die Zeile braucht sie dort nicht)
|
||||||
@@ -418,6 +418,19 @@ test('actionErrorText (D14): bei reply, approve-preview und start-build heißt
|
|||||||
for (const action of ['reply', 'approve-preview', 'start-build']) assert.equal(actionErrorText(down, action), UNCERTAIN_TEXT, action)
|
for (const action of ['reply', 'approve-preview', 'start-build']) assert.equal(actionErrorText(down, action), UNCERTAIN_TEXT, action)
|
||||||
// alle anderen Aktionen und alle echten Antworten des Dienstes behalten ihren Satz
|
// alle anderen Aktionen und alle echten Antworten des Dienstes behalten ihren Satz
|
||||||
for (const action of ['paste', 'claim', 'sent', 'discard', 'done', undefined]) assert.equal(actionErrorText(down, action), down.message, String(action))
|
for (const action of ['paste', 'claim', 'sent', 'discard', 'done', undefined]) assert.equal(actionErrorText(down, action), down.message, String(action))
|
||||||
|
// W1: auch die Fehlerseite des Proxys beim Warten (502/504, Meldung „nicht erreichbar“) lässt offen, ob der Dienst fertig wurde; 503 (kein Upstream) und der Rest nicht
|
||||||
|
for (const status of [502, 504]) {
|
||||||
|
const gateway = Object.assign(new Error('Der Hustler-Dienst ist gerade nicht erreichbar.'), { status })
|
||||||
|
for (const action of ['reply', 'approve-preview', 'start-build']) assert.equal(actionErrorText(gateway, action), UNCERTAIN_TEXT, `${action} ${status}`)
|
||||||
|
for (const action of ['paste', 'claim', 'sent', 'discard', 'done', undefined]) assert.equal(actionErrorText(gateway, action), gateway.message, `${action} ${status}`)
|
||||||
|
// ein Satz des Dienstes unter demselben Status gehört dem Dienst (502 „Anmeldedienst nicht erreichbar.“ kommt vor der Aktion)
|
||||||
|
const sentence = Object.assign(new Error('Anmeldedienst nicht erreichbar.'), { status })
|
||||||
|
assert.equal(actionErrorText(sentence, 'reply'), 'Anmeldedienst nicht erreichbar.', String(status))
|
||||||
|
}
|
||||||
|
for (const status of [503, 429, 500, 401, 400, 409]) {
|
||||||
|
const other = Object.assign(new Error('Der Hustler-Dienst ist gerade nicht erreichbar.'), { status })
|
||||||
|
assert.equal(actionErrorText(other, 'reply'), other.message, String(status))
|
||||||
|
}
|
||||||
const conflict = Object.assign(new Error('Wird schon von einem Kollegen bearbeitet.'), { status: 409 })
|
const conflict = Object.assign(new Error('Wird schon von einem Kollegen bearbeitet.'), { status: 409 })
|
||||||
assert.equal(actionErrorText(conflict, 'reply'), 'Wird schon von einem Kollegen bearbeitet.')
|
assert.equal(actionErrorText(conflict, 'reply'), 'Wird schon von einem Kollegen bearbeitet.')
|
||||||
assert.equal(actionErrorText(Object.assign(new Error('Anfrage zu groß.'), { status: 413 }), 'reply'), 'Anfrage zu groß.')
|
assert.equal(actionErrorText(Object.assign(new Error('Anfrage zu groß.'), { status: 413 }), 'reply'), 'Anfrage zu groß.')
|
||||||
|
|||||||
Reference in new issue
Block a user