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.
|
// cleanup, whose counters were spent deliberately.
|
||||||
const candidates = await resourcesDb.runsNeedingCleanup(CLEANUP_RUN_BATCH, MAX_REVERT_ATTEMPTS)
|
const candidates = await resourcesDb.runsNeedingCleanup(CLEANUP_RUN_BATCH, MAX_REVERT_ATTEMPTS)
|
||||||
let swept = 0
|
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) {
|
for (const candidate of candidates) {
|
||||||
if (!runsDb.TERMINAL.includes(candidate.status)) continue
|
if (!runsDb.TERMINAL.includes(candidate.status)) continue
|
||||||
try {
|
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 = {
|
module.exports = {
|
||||||
STEP_KIND,
|
STEP_KIND,
|
||||||
|
finishedRunsWithNothingLeft,
|
||||||
HELD,
|
HELD,
|
||||||
UNRESOLVED,
|
UNRESOLVED,
|
||||||
reserve,
|
reserve,
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ beforeEach(() => {
|
|||||||
...new Set([...store.rows.values()].filter((r) => ['pending', 'confirmed'].includes(r.status)).map((r) => r.owner_module)),
|
...new Set([...store.rows.values()].filter((r) => ['pending', 'confirmed'].includes(r.status)).map((r) => r.owner_module)),
|
||||||
]
|
]
|
||||||
resourcesDb.runsNeedingCleanup = async () => store.candidates || []
|
resourcesDb.runsNeedingCleanup = async () => store.candidates || []
|
||||||
|
resourcesDb.finishedRunsWithNothingLeft = async () => store.nothingLeft || []
|
||||||
|
|
||||||
runsDb.setCleanupStatus = async (id, to, from = null) => {
|
runsDb.setCleanupStatus = async (id, to, from = null) => {
|
||||||
if (from && !from.includes(store.cleanupStatus)) return false
|
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' })
|
await cleanup.expireResource('demo', { kind: 'zone', ref: 'y' })
|
||||||
assert.equal(store.cleanupStatus, 'incomplete')
|
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