fix: serialize provenance capture for shared extract roots

Protect each shared extraction target with a path-scoped asynchronous handoff so concurrent packages cannot claim each other's files between provenance snapshots. Independent extract directories remain parallel, and the handoff is released on both success and failure.

Add a concurrent two-package regression proving delayed entry and one attributed output per package.
This commit is contained in:
Sucukdeluxe
2026-08-22 12:34:42 +02:00
parent 36cb60fee9
commit 7a052e995c
2 changed files with 78 additions and 13 deletions
+33 -13
View File
@@ -1908,7 +1908,9 @@ export class DownloadManager extends EventEmitter {
private runtimePersistedTotalMs = 0;
private runtimePersistedAt = 0;
private cleanupQueue: Promise<void> = Promise.resolve();
private cleanupQueue: Promise<void> = Promise.resolve();
private packageOutputProvenanceTails = new Map<string, Promise<void>>();
private packagePostProcessQueue: Promise<void> = Promise.resolve();
@@ -4410,6 +4412,30 @@ export class DownloadManager extends EventEmitter {
pkg.outputCount = Math.max(pkg.outputCount || 0, provenance.size);
}
private async runWithPackageOutputProvenance<T>(pkg: PackageEntry, operation: () => Promise<T>): Promise<T> {
const key = pathKey(pkg.extractDir);
const previous = this.packageOutputProvenanceTails.get(key) || Promise.resolve();
let release!: () => void;
const current = new Promise<void>((resolve) => {
release = resolve;
});
this.packageOutputProvenanceTails.set(key, current);
await previous;
const before = await this.snapshotPackageOutputFiles(pkg.extractDir);
try {
return await operation();
} finally {
try {
await this.recordPackageOutputFiles(pkg, before);
} finally {
release();
if (this.packageOutputProvenanceTails.get(key) === current) {
this.packageOutputProvenanceTails.delete(key);
}
}
}
}
private async removeEmptyDirectoryTree(rootDir: string): Promise<number> {
if (!rootDir) {
return 0;
@@ -13265,8 +13291,7 @@ export class DownloadManager extends EventEmitter {
return 0;
}
const packageOutputBefore = await this.snapshotPackageOutputFiles(pkg.extractDir);
const result = await extractPackageArchives({
const result = await this.runWithPackageOutputProvenance(pkg, () => extractPackageArchives({
packageDir: pkg.outputDir,
targetDir: pkg.extractDir,
cleanupMode: this.settings.cleanupMode,
@@ -13424,8 +13449,7 @@ export class DownloadManager extends EventEmitter {
this.emitState();
}
}
});
await this.recordPackageOutputFiles(pkg, packageOutputBefore);
}));
logger.info(`Hybrid-Extract Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}`);
this.logPackageForPackage(pkg, "INFO", "Hybrid-Extract abgeschlossen", {
@@ -13842,8 +13866,7 @@ export class DownloadManager extends EventEmitter {
entry.updatedAt = pendingAt;
}
this.emitState();
const packageOutputBefore = await this.snapshotPackageOutputFiles(pkg.extractDir);
const result = await extractPackageArchives({
const result = await this.runWithPackageOutputProvenance(pkg, () => extractPackageArchives({
packageDir: pkg.outputDir,
targetDir: pkg.extractDir,
cleanupMode: this.settings.cleanupMode,
@@ -13984,8 +14007,7 @@ export class DownloadManager extends EventEmitter {
}
emitExtractStatus(overallLabel);
}
});
await this.recordPackageOutputFiles(pkg, packageOutputBefore);
}));
logger.info(`Post-Processing Entpacken Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}, lastError=${result.lastError || ""}`);
this.logPackageForPackage(pkg, "INFO", "Post-Processing Entpacken Ende", {
extracted: result.extracted,
@@ -14192,8 +14214,7 @@ export class DownloadManager extends EventEmitter {
});
const nestedFailureCategories = new Map<string, string>();
const nestedItems = pkg.itemIds.map((itemId) => this.session.items[itemId]).filter(Boolean) as DownloadItem[];
const packageOutputBefore = await this.snapshotPackageOutputFiles(pkg.extractDir);
const nestedResult = await extractPackageArchives({
const nestedResult = await this.runWithPackageOutputProvenance(pkg, () => extractPackageArchives({
packageDir: pkg.extractDir,
targetDir: pkg.extractDir,
cleanupMode: this.settings.cleanupMode,
@@ -14218,9 +14239,8 @@ export class DownloadManager extends EventEmitter {
nestedFailureCategories.get(progress.archiveName.toLowerCase()) || ""
);
}
});
}));
throwIfAborted();
await this.recordPackageOutputFiles(pkg, packageOutputBefore);
extractedCount += nestedResult.extracted;
logger.info(`Deferred Nested-Extraction Ende: extracted=${nestedResult.extracted}, failed=${nestedResult.failed}`);
this.logPackageForPackage(pkg, "INFO", "Deferred Nested-Extraction Ende", {