Fix parallel extraction false positives
This commit is contained in:
parent
d8535990ae
commit
113b34fadf
@ -651,6 +651,23 @@ export function classifyExtractionError(errorText: string): ExtractErrorCategory
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldSerialRetryParallelFailures(
|
||||||
|
extractedCount: number,
|
||||||
|
failedCategories: ExtractErrorCategory[]
|
||||||
|
): boolean {
|
||||||
|
if (failedCategories.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (extractedCount > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return failedCategories.every((category) =>
|
||||||
|
category === "crc_error"
|
||||||
|
|| category === "wrong_password"
|
||||||
|
|| category === "unknown"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldFallbackLegacyRarToJvm(
|
export function shouldFallbackLegacyRarToJvm(
|
||||||
archivePath: string,
|
archivePath: string,
|
||||||
configuredMode: ExtractBackendMode,
|
configuredMode: ExtractBackendMode,
|
||||||
@ -3001,6 +3018,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
let learnedPassword = cachedPackagePassword;
|
let learnedPassword = cachedPackagePassword;
|
||||||
let packageNeedsFlatMode = false;
|
let packageNeedsFlatMode = false;
|
||||||
const extractedArchives = new Set<string>();
|
const extractedArchives = new Set<string>();
|
||||||
|
const failedArchiveCategories = new Map<string, ExtractErrorCategory>();
|
||||||
for (const archivePath of candidates) {
|
for (const archivePath of candidates) {
|
||||||
if (resumeCompleted.has(archiveNameKey(path.basename(archivePath)))) {
|
if (resumeCompleted.has(archiveNameKey(path.basename(archivePath)))) {
|
||||||
extractedArchives.add(archivePath);
|
extractedArchives.add(archivePath);
|
||||||
@ -3201,6 +3219,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
}
|
}
|
||||||
extracted += 1;
|
extracted += 1;
|
||||||
extractedArchives.add(archivePath);
|
extractedArchives.add(archivePath);
|
||||||
|
failedArchiveCategories.delete(archivePath);
|
||||||
resumeCompleted.add(archiveResumeKey);
|
resumeCompleted.add(archiveResumeKey);
|
||||||
await writeExtractResumeState(options.packageDir, resumeCompleted, options.packageId);
|
await writeExtractResumeState(options.packageDir, resumeCompleted, options.packageId);
|
||||||
logger.info(`Entpacken erfolgreich: ${path.basename(archivePath)}`);
|
logger.info(`Entpacken erfolgreich: ${path.basename(archivePath)}`);
|
||||||
@ -3224,6 +3243,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
failed += 1;
|
failed += 1;
|
||||||
lastError = errorText;
|
lastError = errorText;
|
||||||
const errorCategory = classifyExtractionError(errorText);
|
const errorCategory = classifyExtractionError(errorText);
|
||||||
|
failedArchiveCategories.set(archivePath, errorCategory);
|
||||||
const hintedError = error as ExtractionErrorWithHints;
|
const hintedError = error as ExtractionErrorWithHints;
|
||||||
options.onArchiveFailure?.({
|
options.onArchiveFailure?.({
|
||||||
archiveName,
|
archiveName,
|
||||||
@ -3329,6 +3349,33 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
|
|
||||||
if (abortError) throw new Error("aborted:extract");
|
if (abortError) throw new Error("aborted:extract");
|
||||||
|
|
||||||
|
if (failed > 0 && extracted === 0) {
|
||||||
|
const failedArchives = parallelQueue.filter((ap) => !extractedArchives.has(ap) && !resumeCompleted.has(archiveNameKey(path.basename(ap))));
|
||||||
|
const failedCategories = failedArchives.map((archivePath) => failedArchiveCategories.get(archivePath) || "unknown");
|
||||||
|
if (failedArchives.length > 0 && shouldSerialRetryParallelFailures(extracted, failedCategories)) {
|
||||||
|
const categorySummary = [...new Set(failedCategories)].join(",");
|
||||||
|
logger.info(
|
||||||
|
`Serielle Wiederholung nach Parallel-Fehlstart: ${failedArchives.length} Archive werden einzeln wiederholt ` +
|
||||||
|
`(categories=${categorySummary || "unknown"})`
|
||||||
|
);
|
||||||
|
let retryRecovered = 0;
|
||||||
|
for (const archivePath of failedArchives) {
|
||||||
|
if (options.signal?.aborted || noExtractorEncountered) break;
|
||||||
|
try {
|
||||||
|
failed -= 1;
|
||||||
|
await extractSingleArchive(archivePath);
|
||||||
|
retryRecovered += 1;
|
||||||
|
} catch (retryError) {
|
||||||
|
const errText = String(retryError);
|
||||||
|
if (isExtractAbortError(errText)) throw retryError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (retryRecovered > 0) {
|
||||||
|
logger.info(`Serielle Wiederholung nach Parallel-Fehlstart: ${retryRecovered}/${failedArchives.length} Archive erfolgreich entpackt`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Retry failed wrong_password archives serially ──
|
// ── Retry failed wrong_password archives serially ──
|
||||||
// Parallel UnRAR processes writing to the same target directory can cause
|
// Parallel UnRAR processes writing to the same target directory can cause
|
||||||
// CRC mismatches that are misreported as "Incorrect password".
|
// CRC mismatches that are misreported as "Incorrect password".
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
archiveFilenamePasswords,
|
archiveFilenamePasswords,
|
||||||
detectArchiveSignature,
|
detectArchiveSignature,
|
||||||
classifyExtractionError,
|
classifyExtractionError,
|
||||||
|
shouldSerialRetryParallelFailures,
|
||||||
findArchiveCandidates,
|
findArchiveCandidates,
|
||||||
orderExtractorCandidatesForArchive,
|
orderExtractorCandidatesForArchive,
|
||||||
resolveExtractorBackendModeForArchive,
|
resolveExtractorBackendModeForArchive,
|
||||||
@ -1048,6 +1049,19 @@ describe("extractor", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("shouldSerialRetryParallelFailures", () => {
|
||||||
|
it("keeps serial recovery enabled after mixed parallel results", () => {
|
||||||
|
expect(shouldSerialRetryParallelFailures(1, ["wrong_password"])).toBe(true);
|
||||||
|
expect(shouldSerialRetryParallelFailures(2, ["missing_parts"])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only retries a total parallel wipe-out for contention-like failures", () => {
|
||||||
|
expect(shouldSerialRetryParallelFailures(0, ["crc_error", "wrong_password", "unknown"])).toBe(true);
|
||||||
|
expect(shouldSerialRetryParallelFailures(0, ["missing_parts"])).toBe(false);
|
||||||
|
expect(shouldSerialRetryParallelFailures(0, ["unsupported_format", "crc_error"])).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("password discovery", () => {
|
describe("password discovery", () => {
|
||||||
it("reports per-archive failures through onArchiveFailure", async () => {
|
it("reports per-archive failures through onArchiveFailure", async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-failure-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-failure-"));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user