feat(events): an expired ledger status and ctx.events.expired (MODULE_API 1.11.0)
#209
@@ -320,6 +320,16 @@ async function sweep() {
|
||||
// cleanup, whose counters were spent deliberately.
|
||||
const candidates = await resourcesDb.runsNeedingCleanup(CLEANUP_RUN_BATCH, MAX_REVERT_ATTEMPTS)
|
||||
let swept = 0
|
||||
|
||||
// **A finished run with nothing left to give back is complete** (MODULE_API
|
||||
// 1.11.0). The scan above joins on an unresolved row, and until `expired` no
|
||||
// run could end `pending` without one. Now a zone can expire while its run is
|
||||
// still going — `expireResource` leaves a live run's status to its terminal
|
||||
// path — and when that run ends, nothing in the scan would ever select it:
|
||||
// `pending` for ever. Found by the step-2 walk, run 46.
|
||||
for (const runId of await resourcesDb.finishedRunsWithNothingLeft(CLEANUP_RUN_BATCH)) {
|
||||
if (await runsDb.setCleanupStatus(runId, 'complete', ['pending'])) swept += 1
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
if (!runsDb.TERMINAL.includes(candidate.status)) continue
|
||||
try {
|
||||
|
||||
@@ -402,8 +402,30 @@ async function runsNeedingCleanup(limit = 25, maxAttempts = 3) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finished runs still marked `pending` that have no unresolved row at all — a
|
||||
* run whose last resource the game expired while it was still going (MODULE_API
|
||||
* 1.11.0). The sweep settles them `complete`; `runsNeedingCleanup` cannot see
|
||||
* them because it joins on an unresolved row.
|
||||
*/
|
||||
async function finishedRunsWithNothingLeft(limit = 25) {
|
||||
const rows = await query(
|
||||
`SELECT r.id FROM event_runs r
|
||||
WHERE r.status IN ('completed', 'cancelled', 'failed', 'missed')
|
||||
AND r.cleanup_status = 'pending'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM event_run_resources res
|
||||
WHERE res.run_id = r.id AND res.status IN (?, ?, ?, ?, ?))
|
||||
ORDER BY r.id
|
||||
LIMIT ?`,
|
||||
[...UNRESOLVED, Number(limit)],
|
||||
)
|
||||
return rows.map((row) => Number(row.id))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
STEP_KIND,
|
||||
finishedRunsWithNothingLeft,
|
||||
HELD,
|
||||
UNRESOLVED,
|
||||
reserve,
|
||||
|
||||
@@ -101,6 +101,7 @@ beforeEach(() => {
|
||||
...new Set([...store.rows.values()].filter((r) => ['pending', 'confirmed'].includes(r.status)).map((r) => r.owner_module)),
|
||||
]
|
||||
resourcesDb.runsNeedingCleanup = async () => store.candidates || []
|
||||
resourcesDb.finishedRunsWithNothingLeft = async () => store.nothingLeft || []
|
||||
|
||||
runsDb.setCleanupStatus = async (id, to, from = null) => {
|
||||
if (from && !from.includes(store.cleanupStatus)) return false
|
||||
@@ -728,3 +729,20 @@ test('a run still in flight keeps its cleanup status, and so does one with rows
|
||||
await cleanup.expireResource('demo', { kind: 'zone', ref: 'y' })
|
||||
assert.equal(store.cleanupStatus, 'incomplete')
|
||||
})
|
||||
|
||||
test('a run whose last resource expired while it ran is settled complete when it ends', async () => {
|
||||
// The step-2 walk's run 46: the zone expired while the run was still going, so
|
||||
// expireResource left its status to the terminal path — and the sweep's scan,
|
||||
// which joins on an unresolved row, would never have selected it. `pending`
|
||||
// for ever, on a ledger with nothing left in it.
|
||||
store.cleanupStatus = 'pending'
|
||||
store.candidates = []
|
||||
store.nothingLeft = [RUN.id]
|
||||
assert.equal(await cleanup.sweep(), 1)
|
||||
assert.equal(store.cleanupStatus, 'complete')
|
||||
|
||||
// And never over an incomplete one — that is a human's to clear.
|
||||
store.cleanupStatus = 'incomplete'
|
||||
await cleanup.sweep()
|
||||
assert.equal(store.cleanupStatus, 'incomplete')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user