67 lines
3.4 KiB
JavaScript
67 lines
3.4 KiB
JavaScript
// Closure ledger for terminal wiki recovery attempts.
|
|||
|
|
//
|
||
|
|
// A recovery attempt that reached a terminal budget state can never continue, so
|
||
|
|
// it must not block unrelated first attempts forever. Closing it is a reviewed
|
||
|
|
// decision: it needs an explicit record naming the exact attempt, its terminal
|
||
|
|
// status and error, a written reason and a timestamp. Anything that does not
|
||
|
|
// match the row we are looking at fails closed, and a pending attempt can never
|
||
|
|
// be closed while it is still running.
|
||
|
|
|
||
|
|
const fs = require('node:fs')
|
||
|
|
const path = require('node:path')
|
||
|
|
const assert = require('node:assert/strict')
|
||
|
|
const { terminalRecoveryStatuses } = require('./wiki_recovery_policy.cjs')
|
||
|
|
|
||
|
|
const closureFile = 'closures.json'
|
||
|
|
const closureKey = (tenant, deadLetterId, attempt) => `${tenant}:${deadLetterId}:${attempt}`
|
||
|
|
|
||
|
|
function parseClosures(parsed, tenant) {
|
||
|
|
assert.equal(parsed.tenant_id, tenant, 'Closure ledger belongs to another tenant')
|
||
|
|
assert.ok(Array.isArray(parsed.closures), 'Closure ledger must list closures')
|
||
|
|
const closures = new Map()
|
||
|
|
for (const closure of parsed.closures) {
|
||
|
|
assert.ok(Number.isSafeInteger(closure.dead_letter_id) && closure.dead_letter_id >= 1 && closure.dead_letter_id <= 33,
|
||
|
|
'Only original dead letters 1..33 can be closed')
|
||
|
|
assert.ok(Number.isSafeInteger(closure.attempt_number) && closure.attempt_number >= 1, 'A closure needs the exact attempt number')
|
||
|
|
assert.ok(terminalRecoveryStatuses.includes(closure.terminal_status), 'Only terminal attempts can be closed')
|
||
|
|
assert.match(closure.error_code || '', /^[A-Z0-9_]{3,64}$/, 'A closure needs the recorded terminal error code')
|
||
|
|
assert.ok(typeof closure.reason === 'string' && closure.reason.trim().length >= 12, 'A closure needs a written reason')
|
||
|
|
assert.ok(Number.isFinite(Date.parse(closure.closed_at)), 'A closure needs a reviewable timestamp')
|
||
|
|
const key = closureKey(tenant, closure.dead_letter_id, closure.attempt_number)
|
||
|
|
assert.ok(!closures.has(key), 'Closure ledger cannot close the same attempt twice')
|
||
|
|
closures.set(key, closure)
|
||
|
|
}
|
||
|
|
return closures
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadClosures(directory, tenant) {
|
||
|
|
if (!directory) return new Map()
|
||
|
|
let raw
|
||
|
|
try {
|
||
|
|
raw = fs.readFileSync(path.join(directory, closureFile), 'utf8')
|
||
|
|
} catch (error) {
|
||
|
|
// A missing ledger simply means nothing has been closed yet.
|
||
|
|
if (error.code === 'ENOENT') return new Map()
|
||
|
|
throw error
|
||
|
|
}
|
||
|
|
return parseClosures(JSON.parse(raw), tenant)
|
||
|
|
}
|
||
|
|
|
||
|
|
function closureFor(row, closures, tenant) {
|
||
|
|
const closure = closures?.get(closureKey(row.tenant_id ?? tenant, row.dead_letter_id, Number(row.attempt_number || 1)))
|
||
|
|
if (!closure) return null
|
||
|
|
// The closure names one terminal attempt. A row that is still running (or that
|
||
|
|
// resolved without a terminal budget state) is simply not covered by it yet.
|
||
|
|
if (!terminalRecoveryStatuses.includes(row.recovery_status)) return null
|
||
|
|
assert.ok(!row.pending && !(row.pending_finalize > 0), 'A pending attempt cannot be closed')
|
||
|
|
assert.equal(closure.terminal_status, row.recovery_status, 'Closure does not match the recorded terminal status')
|
||
|
|
assert.equal(closure.error_code, row.error_code, 'Closure does not match the recorded terminal error')
|
||
|
|
return closure
|
||
|
|
}
|
||
|
|
|
||
|
|
function annotateClosed(rows, closures, tenant) {
|
||
|
|
return rows.map(row => ({ ...row, closed: closureFor(row, closures, tenant) !== null }))
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { loadClosures, parseClosures, closureFor, annotateClosed, closureKey, closureFile }
|