From c2403cad7b2fde960bdfdeb4285c99bceb453a75 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Fri, 8 May 2026 10:59:30 -0500 Subject: [PATCH 1/2] Address flaky tests - Prevent extra SBA refresh request after refresh is disabled - Address race condition in thread dump test - Catch network-related SQLServer connection error - Remove retry from eureka server timeout - More buffer for infra around health aggregator parallelization test (allow +1s for +3s work) --- ...RelationalDatabaseHealthContributorTest.cs | 5 ++++- .../EurekaServiceCollectionExtensionsTest.cs | 2 +- .../SpringBootAdminPeriodicRefresh.cs | 21 ++++++++++++------- .../Actuators/Health/HealthAggregationTest.cs | 6 +++--- .../ThreadDump/EventPipeThreadDumperTest.cs | 20 ++++++++++++++---- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs b/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs index 267d293dac..85503ee1a2 100644 --- a/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs +++ b/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs @@ -111,7 +111,10 @@ public async Task SQLServer_Not_Connected_Returns_Down_Status() result.Description.Should().Be("SQL Server health check failed"); result.Details.Should().Contain("host", "localhost"); result.Details.Should().Contain("service", "Example"); - result.Details.Should().ContainKey("error").WhoseValue.As().Should().StartWith("SqlException: Connection Timeout Expired."); + + result.Details.Should().ContainKey("error").WhoseValue.As().Should().Match(exception => + exception.StartsWith("SqlException: Connection Timeout Expired.", StringComparison.InvariantCulture) || + exception.StartsWith("SqlException: A network-related or instance-specific error", StringComparison.InvariantCulture)); } [Fact(Skip = "Integration test - Requires local SQL Server instance")] diff --git a/src/Discovery/test/Eureka.Test/EurekaServiceCollectionExtensionsTest.cs b/src/Discovery/test/Eureka.Test/EurekaServiceCollectionExtensionsTest.cs index 6d22358713..745c012afb 100644 --- a/src/Discovery/test/Eureka.Test/EurekaServiceCollectionExtensionsTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaServiceCollectionExtensionsTest.cs @@ -62,7 +62,7 @@ public async Task AddEurekaDiscoveryClient_UsesServerTimeout() var appSettings = new Dictionary { ["Eureka:Client:EurekaServer:ConnectTimeoutSeconds"] = "1", - ["Eureka:Client:EurekaServer:RetryCount"] = "1" + ["Eureka:Client:EurekaServer:RetryCount"] = "0" }; IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(appSettings).Build(); diff --git a/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs b/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs index 6426993f12..9477238be3 100644 --- a/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs +++ b/src/Management/src/Endpoint/SpringBootAdminClient/SpringBootAdminPeriodicRefresh.cs @@ -44,15 +44,20 @@ private async Task TimerLoopAsync(TimeSpan interval) do { - LogStartingRefreshCycle(); - - try - { - await _runner.RunAsync(isFirstTime, _timerTokenSource.Token); - } - catch (Exception exception) when (!exception.IsCancellation()) + // A tick queued just before periodic refresh was disabled would still be delivered here. + // Checking the period prevents executing a stale tick when refresh has been turned off. + if (isFirstTime || _periodicTimer.Period != Timeout.InfiniteTimeSpan) { - LogRefreshCycleFailed(exception); + LogStartingRefreshCycle(); + + try + { + await _runner.RunAsync(isFirstTime, _timerTokenSource.Token); + } + catch (Exception exception) when (!exception.IsCancellation()) + { + LogRefreshCycleFailed(exception); + } } isFirstTime = false; diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs b/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs index ad76b9eaad..b21104fc60 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs @@ -366,9 +366,9 @@ public async Task Aggregates_contributors_in_parallel() { List contributors = [ - new SlowContributor(1.Seconds()), new SlowContributor(2.Seconds()), - new SlowContributor(3.Seconds()) + new SlowContributor(3.Seconds()), + new SlowContributor(4.Seconds()) ]; WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); @@ -408,7 +408,7 @@ public async Task Aggregates_contributors_in_parallel() } """); - stopwatch.Elapsed.Should().BeGreaterThan(500.Milliseconds()).And.BeLessThan(5.Seconds()); + stopwatch.Elapsed.Should().BeGreaterThan(500.Milliseconds()).And.BeLessThan(6.Seconds()); } [Fact] diff --git a/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs b/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs index 3e46e42afd..6974a7f17d 100644 --- a/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs @@ -16,19 +16,26 @@ public sealed class EventPipeThreadDumperTest public async Task Can_resolve_source_location_from_pdb() { using var backgroundCancellationSource = new CancellationTokenSource(); + using var threadStarted = new ManualResetEventSlim(false); var backgroundThread = new Thread(NestedType.BackgroundThreadCallback) { IsBackground = true }; - backgroundThread.Start(backgroundCancellationSource.Token); + backgroundThread.Start((backgroundCancellationSource.Token, threadStarted)); + threadStarted.Wait(TestContext.Current.CancellationToken); using var loggerProvider = new CapturingLoggerProvider(); using var loggerFactory = new LoggerFactory([loggerProvider]); ILogger logger = loggerFactory.CreateLogger(); - var optionsMonitor = new TestOptionsMonitor(); + // Use a longer collection window to provide enough sample opportunities on a loaded CI runner. + var optionsMonitor = TestOptionsMonitor.Create(new ThreadDumpEndpointOptions + { + Duration = 100 + }); + var dumper = new EventPipeThreadDumper(optionsMonitor, logger); IList threads = await dumper.DumpThreadsAsync(TestContext.Current.CancellationToken); @@ -87,11 +94,16 @@ private static class NestedType { public static void BackgroundThreadCallback(object? argument) { - var cancellationToken = (CancellationToken)argument!; + (CancellationToken cancellationToken, ManualResetEventSlim threadReady) = ((CancellationToken, ManualResetEventSlim))argument!; + + threadReady.Set(); while (!cancellationToken.IsCancellationRequested) { - Thread.Sleep(TimeSpan.FromMilliseconds(50)); + // Only actively-running threads are shown in the thread dump, so we need to make sure the CPU is in use. + // Thread.Sleep(0) yields to allow the EventPipe rundown thread to make progress on .NET 8. + Thread.SpinWait(250); + Thread.Sleep(0); } } } From e2c698a2c1b48c5b912628e01d479dae0aa2caa1 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Mon, 11 May 2026 08:46:30 -0500 Subject: [PATCH 2/2] PR Feedback - allow more time for parallel health check - threadReady -> threadStarted - keep changes for net8 specific to net8 --- .../RelationalDatabaseHealthContributorTest.cs | 4 ++-- .../Actuators/Health/HealthAggregationTest.cs | 3 ++- .../ThreadDump/EventPipeThreadDumperTest.cs | 14 ++++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs b/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs index 85503ee1a2..42da1e5d2d 100644 --- a/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs +++ b/src/Connectors/test/Connectors.Test/RelationalDatabaseHealthContributorTest.cs @@ -113,8 +113,8 @@ public async Task SQLServer_Not_Connected_Returns_Down_Status() result.Details.Should().Contain("service", "Example"); result.Details.Should().ContainKey("error").WhoseValue.As().Should().Match(exception => - exception.StartsWith("SqlException: Connection Timeout Expired.", StringComparison.InvariantCulture) || - exception.StartsWith("SqlException: A network-related or instance-specific error", StringComparison.InvariantCulture)); + exception.StartsWith("SqlException: Connection Timeout Expired.", StringComparison.Ordinal) || + exception.StartsWith("SqlException: A network-related or instance-specific error", StringComparison.Ordinal)); } [Fact(Skip = "Integration test - Requires local SQL Server instance")] diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs b/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs index b21104fc60..172f589c1c 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs @@ -408,7 +408,8 @@ public async Task Aggregates_contributors_in_parallel() } """); - stopwatch.Elapsed.Should().BeGreaterThan(500.Milliseconds()).And.BeLessThan(6.Seconds()); + // Upper bound must be less than 2+3+4=9s if contributors ran sequentially. + stopwatch.Elapsed.Should().BeGreaterThan(500.Milliseconds()).And.BeLessThan(9.Seconds()); } [Fact] diff --git a/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs b/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs index 6974a7f17d..43711fff72 100644 --- a/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/ThreadDump/EventPipeThreadDumperTest.cs @@ -30,11 +30,15 @@ public async Task Can_resolve_source_location_from_pdb() using var loggerFactory = new LoggerFactory([loggerProvider]); ILogger logger = loggerFactory.CreateLogger(); - // Use a longer collection window to provide enough sample opportunities on a loaded CI runner. +#if NET8_0 + // Use a longer collection window on .NET 8 to compensate for the Sleep(0) yield. var optionsMonitor = TestOptionsMonitor.Create(new ThreadDumpEndpointOptions { Duration = 100 }); +#else + var optionsMonitor = new TestOptionsMonitor(); +#endif var dumper = new EventPipeThreadDumper(optionsMonitor, logger); @@ -94,16 +98,18 @@ private static class NestedType { public static void BackgroundThreadCallback(object? argument) { - (CancellationToken cancellationToken, ManualResetEventSlim threadReady) = ((CancellationToken, ManualResetEventSlim))argument!; + (CancellationToken cancellationToken, ManualResetEventSlim threadStarted) = ((CancellationToken, ManualResetEventSlim))argument!; - threadReady.Set(); + threadStarted.Set(); while (!cancellationToken.IsCancellationRequested) { // Only actively-running threads are shown in the thread dump, so we need to make sure the CPU is in use. - // Thread.Sleep(0) yields to allow the EventPipe rundown thread to make progress on .NET 8. Thread.SpinWait(250); +#if NET8_0 + // Yield to allow the EventPipe rundown thread to make progress on .NET 8. Thread.Sleep(0); +#endif } } }