From ae7116334931e76c191a35a0fc3131f169d65beb Mon Sep 17 00:00:00 2001 From: jsboige Date: Mon, 29 Jun 2026 13:20:47 +0200 Subject: [PATCH] fix(pipeline): #613 make release harvest resilient to per-set failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single card set whose harvest timed out (e.g. a large set — Virtues 113, Scenarii 174, Fallacies 1408 — under the parallelism the optimizer ramps to 6) threw out of the nested Parallel.ForEachAsync and aborted the ENTIRE 8-language release régén, losing every not-yet-processed set (observed 2026-06-29). Now HarvestImages attempts every reachable set, persists all successful harvests to disk, and collects failures. If any set failed it logs a loud [HARVEST-PARTIAL] summary and raises a single aggregate ApplicationException at the END (listing all failed sets) instead of on the first failure — so partial runs are never silent and a re-run skips the good harvests (cache) and only re-collects the missing ones. Gated by new WebBasedGeneratorConfig.ContinueOnHarvestSetFailure (default true); set false to restore the legacy abort-on-first-failure behavior. Build + 549/0/5 unit tests pass. Full régén validation runs via po-2023 serial pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Cardpen/HarvestManager.cs | 27 ++++++++++++++++++- .../WebBasedGeneratorConfig.cs | 10 +++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Generation/Converters/Argumentum.AssetConverter/WebBasedGenerator/Cardpen/HarvestManager.cs b/Generation/Converters/Argumentum.AssetConverter/WebBasedGenerator/Cardpen/HarvestManager.cs index 3ffe913c8..7b42c9a15 100644 --- a/Generation/Converters/Argumentum.AssetConverter/WebBasedGenerator/Cardpen/HarvestManager.cs +++ b/Generation/Converters/Argumentum.AssetConverter/WebBasedGenerator/Cardpen/HarvestManager.cs @@ -82,6 +82,11 @@ private async Task GetBrowserAsync() var funcBrowser = new Func>(GetBrowserAsync); + // Issue #613: collect per-set failures so one set timing out (e.g. a large set under high + // parallelism) does not abort the whole multi-language harvest. All reachable sets are + // attempted and persisted; a single aggregate error is raised at the end if any failed. + var failedSets = new ConcurrentBag<(string cardSet, string language, string error)>(); + var parallelOptionsCardset = new ParallelOptions { MaxDegreeOfParallelism = Config.EnableParallelism? Config.MaxDegreeOfParallelismCardpen : 1 }; await Parallel.ForEachAsync(targetCardSets, parallelOptionsCardset, async (configCardSet, token) => { @@ -89,10 +94,30 @@ await Parallel.ForEachAsync(targetCardSets, parallelOptionsCardset, async (confi var parallelOptionsCardsetLanguage = new ParallelOptions { MaxDegreeOfParallelism = Config.EnableParallelism? Config.MaxDegreeOfParallelismCardpenTranslations : 1 }; await Parallel.ForEachAsync(targetLanguages, parallelOptionsCardsetLanguage, async (currentLanguage, newToken) => { - await ProcessLocalizedHarvest(configCardSet, currentLanguage, harvestDictionary, funcBrowser); + try + { + await ProcessLocalizedHarvest(configCardSet, currentLanguage, harvestDictionary, funcBrowser); + } + catch (Exception ex) when (Config.ContinueOnHarvestSetFailure) + { + // Persist the failure and keep harvesting the other sets (issue #613). + failedSets.Add((configCardSet.Name, currentLanguage, ex.Message)); + Logger.Log($"[HARVEST-FAILURE] Card set '{configCardSet.Name}' / '{currentLanguage}' failed and was skipped: {ex.Message}", MessageType.Problem); + } }); }); + if (!failedSets.IsEmpty) + { + var summary = string.Join("; ", failedSets.Select(f => $"{f.cardSet}/{f.language}")); + Logger.Log($"[HARVEST-PARTIAL] {failedSets.Count} card set(s) failed to harvest and were skipped: {summary}. " + + "All successful harvests were persisted to disk — re-run to collect the missing sets (the cache skips the good ones). " + + "If large sets time out, set EnableParallelism=false or lower MaxDegreeOfParallelismCardpen (issue #613).", MessageType.Problem); + throw new ApplicationException( + $"Harvest completed with {failedSets.Count} failed card set(s): {summary}. " + + "Successful harvests were persisted; re-run to collect the missing sets (issue #613)."); + } + return harvestDictionary; } diff --git a/Generation/Converters/Argumentum.AssetConverter/WebBasedGenerator/WebBasedGeneratorConfig.cs b/Generation/Converters/Argumentum.AssetConverter/WebBasedGenerator/WebBasedGeneratorConfig.cs index f767fa4be..c72a8e935 100644 --- a/Generation/Converters/Argumentum.AssetConverter/WebBasedGenerator/WebBasedGeneratorConfig.cs +++ b/Generation/Converters/Argumentum.AssetConverter/WebBasedGenerator/WebBasedGeneratorConfig.cs @@ -34,6 +34,16 @@ public class WebBasedGeneratorConfig public int MaxDegreeOfParallelismCardpenTranslations { get; set; } = 2; + /// + /// Issue #613: when true (default), a card set whose harvest fails (e.g. a large set + /// timing out under high parallelism) is logged and skipped so the remaining sets still + /// harvest and persist to disk; a single aggregate error is raised at the end of the run + /// listing every failed set. A re-run then skips the good harvests (cache) and only + /// re-collects the missing ones. When false, the first failure aborts the whole harvest + /// (legacy behavior). + /// + public bool ContinueOnHarvestSetFailure { get; set; } = true; + public int MaxDegreeOfParallelismImages { get; set; } = 3; public int MaxDegreeOfParallelismImageTranslations { get; set; } = 2;