fix(backups): better handling of generator cleanup (#9557)

This commit is contained in:
Florent BEAUCHAMP
2026-03-10 15:55:59 +01:00
committed by GitHub
parent a78870b473
commit 4e0655dc3d
9 changed files with 121 additions and 43 deletions

View File

@@ -48,17 +48,10 @@ export abstract class Disk {
abstract buildDiskBlockGenerator(): Promise<AsyncGenerator<DiskBlock>> | AsyncGenerator<DiskBlock>
async *diskBlocks(uid?: string): AsyncGenerator<DiskBlock> {
try {
// compute next block while the destination is consuming the current block
const blockGenerator = await this.buildDiskBlockGenerator()
let next = blockGenerator.next()
while (true) {
const res = await next
next = blockGenerator.next()
if (res.done) {
break
}
for await (const block of blockGenerator) {
this.#generatedDiskBlocks++
yield res.value
yield block
}
} finally {
await this.close()

View File

@@ -28,7 +28,7 @@ test('Disk class', async t => {
disk.blocks[key] = true
}
const foundKeys = []
const generator = disk.diskBlocks()
const generator = disk.diskBlocks()
for await (const block of generator) {
foundKeys.push(block.index)
assert.strictEqual(block.data.length, blockSize)
@@ -39,4 +39,24 @@ test('Disk class', async t => {
foundKeys.sort()
assert.deepStrictEqual(keys, foundKeys)
})
await t.test('diskBlocks calls progressHandler.done when consumer exits early', async () => {
let doneCalled = false
disk.progressHandler = {
setProgress: async () => {},
done: async () => {
doneCalled = true
},
}
disk.blocks[4] = true
disk.blocks[5] = true
disk.blocks[6] = true
// break after the first block — the inner generator's finally must still run
for await (const _ of disk.diskBlocks()) {
break
}
assert.strictEqual(doneCalled, true)
})
})

View File

@@ -24,12 +24,14 @@ export class ReadAhead extends RandomDiskPassthrough {
const PRELOAD_SIZE = Math.max(Math.min(Math.floor((blockIndexes.length * maxPercent) / 100), maxNumber), 1)
let counter = 0
try{
try {
for (const index of blockIndexes) {
counter++
await self.progressHandler?.setProgress(counter/ blockIndexes.length)
await self.progressHandler?.setProgress(counter / blockIndexes.length)
if (preloaded.length < PRELOAD_SIZE) {
preloaded.push(self.source.readBlock(index))
const p = self.source.readBlock(index)
p.catch(() => {}) // suppress unhandled rejection; error still surfaces when awaited via shift()
preloaded.push(p)
}
if (preloaded.length === PRELOAD_SIZE) {
const next = (await preloaded.shift())!
@@ -40,10 +42,11 @@ export class ReadAhead extends RandomDiskPassthrough {
const next = (await preloaded.shift())!
yield next
}
}finally{
} finally {
await Promise.allSettled(preloaded) // wait for in-flight I/O & release resources
preloaded.length = 0
await self.progressHandler?.done()
}
}
return generator()
}