From 411d7c52bbc5348f68a7c952d41b89e07c74d5dc Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 6 Jun 2026 00:33:51 +0900 Subject: [PATCH 1/3] Fix diagnostic sanitizer timeout fallback (#3292) --- changelog.d/unreleased/3292.fixed.md | 16 ++++++++ .../Diagnostics/DiagnosticSanitizer.cs | 40 ++++++++++++++++++- .../DiagnosticSanitizerTests.cs | 30 ++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 changelog.d/unreleased/3292.fixed.md create mode 100644 tests/CodeIndex.Tests/DiagnosticSanitizerTests.cs diff --git a/changelog.d/unreleased/3292.fixed.md b/changelog.d/unreleased/3292.fixed.md new file mode 100644 index 0000000000..df766fe133 --- /dev/null +++ b/changelog.d/unreleased/3292.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3292 +affected: + - src/CodeIndex/Diagnostics/DiagnosticSanitizer.cs + - tests/CodeIndex.Tests/DiagnosticSanitizerTests.cs +--- + +## English + +- **Diagnostic sanitization no longer leaks regex timeout exceptions (#3292)** — plugin and pattern diagnostics now fall back to a generic sanitized message if path redaction hits its bounded regex timeout, so full-suite runs do not fail while reporting plugin registry status. + +## 日本語 + +- **診断 sanitization が regex timeout 例外を漏らさないようになりました (#3292)** — plugin / pattern 診断の path redaction が上限付き regex timeout に達した場合は汎用の sanitized message に fallback するため、plugin registry status の報告中に full suite が失敗しなくなりました。 diff --git a/src/CodeIndex/Diagnostics/DiagnosticSanitizer.cs b/src/CodeIndex/Diagnostics/DiagnosticSanitizer.cs index f8b1d09f60..a17c85f7f1 100644 --- a/src/CodeIndex/Diagnostics/DiagnosticSanitizer.cs +++ b/src/CodeIndex/Diagnostics/DiagnosticSanitizer.cs @@ -5,6 +5,8 @@ namespace CodeIndex.Diagnostics; internal static class DiagnosticSanitizer { private const int MaxDiagnosticFieldLength = 240; + private const int MaxSanitizerInputLength = MaxDiagnosticFieldLength * 8; + internal const string RegexTimeoutFallbackMessage = "[message omitted after sanitization timeout]"; private static readonly Regex AbsolutePathPattern = new( @"(?:[A-Za-z]:)?[/\\][^\s'"";:,)]+", RegexOptions.Compiled | RegexOptions.CultureInvariant, @@ -32,6 +34,9 @@ public static string ForPath(string? path) => string.IsNullOrWhiteSpace(value) ? value : ForMessage(value); public static string ForMessage(string? message) + => ForMessage(message, value => AbsolutePathPattern.Replace(value, "")); + + internal static string ForMessage(string? message, Func redactPaths) { if (string.IsNullOrWhiteSpace(message)) return string.Empty; @@ -40,8 +45,18 @@ public static string ForMessage(string? message) .Replace('\r', ' ') .Replace('\n', ' ') .Replace('\t', ' '); - var withoutPaths = AbsolutePathPattern.Replace(singleLine, ""); - return Truncate(Regex.Replace(withoutPaths, @"\s{2,}", " ", RegexOptions.None, TimeSpan.FromMilliseconds(50)).Trim()); + if (singleLine.Length > MaxSanitizerInputLength) + singleLine = singleLine[..MaxSanitizerInputLength] + " ..."; + + try + { + var withoutPaths = redactPaths(singleLine); + return Truncate(CollapseWhitespace(withoutPaths).Trim()); + } + catch (RegexMatchTimeoutException) + { + return RegexTimeoutFallbackMessage; + } } private static string TryGetFullPath(string path) @@ -59,6 +74,27 @@ private static string TryGetFullPath(string path) private static string NormalizeSeparators(string value) => value.Replace('\\', '/'); + private static string CollapseWhitespace(string value) + { + var collapsed = new System.Text.StringBuilder(value.Length); + var previousWasWhitespace = false; + foreach (var character in value) + { + if (char.IsWhiteSpace(character)) + { + if (!previousWasWhitespace) + collapsed.Append(' '); + previousWasWhitespace = true; + continue; + } + + collapsed.Append(character); + previousWasWhitespace = false; + } + + return collapsed.ToString(); + } + private static string Truncate(string value) => value.Length <= MaxDiagnosticFieldLength ? value diff --git a/tests/CodeIndex.Tests/DiagnosticSanitizerTests.cs b/tests/CodeIndex.Tests/DiagnosticSanitizerTests.cs new file mode 100644 index 0000000000..4a6ff5e7c8 --- /dev/null +++ b/tests/CodeIndex.Tests/DiagnosticSanitizerTests.cs @@ -0,0 +1,30 @@ +using System.Text.RegularExpressions; +using CodeIndex.Diagnostics; + +namespace CodeIndex.Tests; + +public class DiagnosticSanitizerTests +{ + [Fact] + public void ForMessage_RedactsPathsAndCollapsesWhitespace() + { + var sanitized = DiagnosticSanitizer.ForMessage("failed\nat /tmp/codeindex/plugins/bad.dll\twith details"); + + Assert.Equal("failed at with details", sanitized); + } + + [Fact] + public void ForMessage_RedactionTimeout_ReturnsFallbackMessage() + { + var timeout = new RegexMatchTimeoutException( + "load failed at /tmp/codeindex/plugins/bad.dll", + "path", + TimeSpan.FromMilliseconds(50)); + + var sanitized = DiagnosticSanitizer.ForMessage( + "load failed at /tmp/codeindex/plugins/bad.dll", + _ => throw timeout); + + Assert.Equal(DiagnosticSanitizer.RegexTimeoutFallbackMessage, sanitized); + } +} From b335412c16d7e703b59e5781ff5287fa9230a9c0 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 6 Jun 2026 00:35:36 +0900 Subject: [PATCH 2/3] Enforce hook callback budget after late responses (#3252) --- changelog.d/unreleased/3252.fixed.md | 15 +++++++++++++++ .../Hooks/PostExtractionHookCallbackWorker.cs | 10 ++++++++++ 2 files changed, 25 insertions(+) create mode 100644 changelog.d/unreleased/3252.fixed.md diff --git a/changelog.d/unreleased/3252.fixed.md b/changelog.d/unreleased/3252.fixed.md new file mode 100644 index 0000000000..740485f981 --- /dev/null +++ b/changelog.d/unreleased/3252.fixed.md @@ -0,0 +1,15 @@ +--- +category: fixed +issues: + - 3252 +affected: + - src/CodeIndex/Indexer/Hooks/PostExtractionHookCallbackWorker.cs +--- + +## English + +- **Post-extraction hook callbacks now reject late over-budget responses (#3252)** — hook worker responses are discarded when the measured callback elapsed time already exceeds `CDIDX_HOOK_CALLBACK_BUDGET_MS`, preventing timed-out mutations from being accepted under full-suite scheduler contention. + +## 日本語 + +- **post-extraction hook callback が遅れて返した budget 超過 response を拒否するようになりました (#3252)** — 測定済み callback elapsed time が `CDIDX_HOOK_CALLBACK_BUDGET_MS` を超えている場合は hook worker response を破棄し、full suite の scheduler contention 下で timeout 済み mutation が採用されないようにしました。 diff --git a/src/CodeIndex/Indexer/Hooks/PostExtractionHookCallbackWorker.cs b/src/CodeIndex/Indexer/Hooks/PostExtractionHookCallbackWorker.cs index be43fb930d..4d012a1672 100644 --- a/src/CodeIndex/Indexer/Hooks/PostExtractionHookCallbackWorker.cs +++ b/src/CodeIndex/Indexer/Hooks/PostExtractionHookCallbackWorker.cs @@ -111,6 +111,13 @@ internal PostExtractionHookCallbackResult Invoke( return Failure($"failed to read worker response: {responseException.Message}", stopwatch.ElapsedMilliseconds); } + if (CallbackBudgetExceeded(stopwatch, callbackBudget)) + { + KillWorker(); + stopwatch.Stop(); + return TimedOut(stopwatch.ElapsedMilliseconds); + } + stopwatch.Stop(); var responseJson = responseTask.GetAwaiter().GetResult(); if (responseJson == null) @@ -300,6 +307,9 @@ private static int GetRemainingWaitMilliseconds(Stopwatch stopwatch, TimeSpan ca return Math.Max(1, (int)Math.Ceiling(Math.Min(remainingMilliseconds, int.MaxValue))); } + private static bool CallbackBudgetExceeded(Stopwatch stopwatch, TimeSpan callbackBudget) + => stopwatch.Elapsed > callbackBudget; + private static string BuildWorkerExitError(Process? process, string stderr, string fallback) { var exitCodeText = process == null From f259ff96ffea0a7439918dd169e38e24d0dd0f90 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 6 Jun 2026 00:37:08 +0900 Subject: [PATCH 3/3] Stabilize hook budget timing test (#3261) --- changelog.d/unreleased/3261.fixed.md | 15 +++++++++++++++ tests/CodeIndex.Tests/PostExtractionHookTests.cs | 8 ++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 changelog.d/unreleased/3261.fixed.md diff --git a/changelog.d/unreleased/3261.fixed.md b/changelog.d/unreleased/3261.fixed.md new file mode 100644 index 0000000000..5586586370 --- /dev/null +++ b/changelog.d/unreleased/3261.fixed.md @@ -0,0 +1,15 @@ +--- +category: fixed +issues: + - 3261 +affected: + - tests/CodeIndex.Tests/PostExtractionHookTests.cs +--- + +## English + +- **PostExtractionHookTests now use wider callback-budget timing margins (#3261)** — the slow hook budget test now keeps the hook delay well beyond the test budget under full-suite load while still checking that timed-out workers are killed before they can signal completion. + +## 日本語 + +- **`PostExtractionHookTests` の callback budget test がより広い timing margin を使うようになりました (#3261)** — slow hook budget test は full suite load 下でも hook delay が test budget を十分に超える余裕を持たせつつ、timeout した worker が completion を通知する前に kill されることを引き続き検証します。 diff --git a/tests/CodeIndex.Tests/PostExtractionHookTests.cs b/tests/CodeIndex.Tests/PostExtractionHookTests.cs index 72a03bb3ef..cd62a270dd 100644 --- a/tests/CodeIndex.Tests/PostExtractionHookTests.cs +++ b/tests/CodeIndex.Tests/PostExtractionHookTests.cs @@ -164,8 +164,8 @@ public void CallbackBudgetExceeded_KillsWorkerAndSkipsTimedOutMutation() var originalBudget = PostExtractionHookRunner.CallbackBudgetForTesting; try { - env.Set(SlowHookDelayEnvironmentVariable, "200"); - PostExtractionHookRunner.CallbackBudgetForTesting = () => TimeSpan.FromMilliseconds(50); + env.Set(SlowHookDelayEnvironmentVariable, "500"); + PostExtractionHookRunner.CallbackBudgetForTesting = () => TimeSpan.FromMilliseconds(100); var hooksDir = Path.Combine(projectRoot, "hooks"); var completionPath = Path.Combine(projectRoot, "slow-hook.done"); env.Set(SlowHookCompletionPathEnvironmentVariable, completionPath); @@ -178,7 +178,7 @@ public void CallbackBudgetExceeded_KillsWorkerAndSkipsTimedOutMutation() var symbols = new List(); runner.OnSymbolsExtracted(context, symbols); - AssertFileDoesNotAppear(completionPath, TimeSpan.FromMilliseconds(750)); + AssertFileDoesNotAppear(completionPath, TimeSpan.FromMilliseconds(1000)); Assert.DoesNotContain(symbols, symbol => symbol.Name == "SlowHookTag"); var diagnostic = Assert.Single( @@ -191,7 +191,7 @@ public void CallbackBudgetExceeded_KillsWorkerAndSkipsTimedOutMutation() // The worker wait can time out at the budget boundary before // ElapsedMilliseconds rounds up to the full budget on some CI hosts. Assert.True(diagnostic.DurationMs > 0); - Assert.Equal(50, (long)Math.Round(runner.CallbackBudget.TotalMilliseconds, MidpointRounding.AwayFromZero)); + Assert.Equal(100, (long)Math.Round(runner.CallbackBudget.TotalMilliseconds, MidpointRounding.AwayFromZero)); } CollectUnloadedHookAssemblies(); }