From d4af908911802c4fa53edcb56bd861cb698116ee Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 12 Aug 2026 16:31:16 -0700 Subject: [PATCH 1/2] fix flaky HeartbeatEndpointSettingsSyncHostedService tests by polling instead of fixed delay --- ...tEndpointSettingsSyncHostedServiceTests.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs index 6b8b1ca1c8..37494dfe49 100644 --- a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs +++ b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs @@ -17,23 +17,34 @@ [TestFixture] public class HeartbeatEndpointSettingsSyncHostedServiceTests { + + static async Task WaitUntilAsync(Func condition, TimeSpan? timeout = null) + { + var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(10)); + while (!condition() && DateTime.UtcNow < deadline) + { + await Task.Delay(20); + } + } + [Test] public async Task Should_handle_cancellation_token_gracefully() { using var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)); CancellationToken token = tokenSource.Token; var fakeTimeProvider = new FakeTimeProvider(); + var mockEndpointInstanceMonitoring = new MockEndpointInstanceMonitoring([]); var service = new HeartbeatEndpointSettingsSyncHostedService( new MockMonitoringDataStore([]), new MockEndpointSettingsStore([]), - new MockEndpointInstanceMonitoring([]), new Settings { TrackInstancesInitialValue = true }, + mockEndpointInstanceMonitoring, new Settings { TrackInstancesInitialValue = true }, fakeTimeProvider, NullLogger.Instance) { DelayStart = TimeSpan.Zero }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointInstanceMonitoring.GetEndpointsCallCount >= 1); await service.StopAsync(token); Assert.That(service.ExecuteTask?.IsCompletedSuccessfully, Is.True); @@ -59,7 +70,7 @@ public async Task Should_delete_settings_from_endpoints_that_are_no_longer_live( }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointSettingsStore.Deleted.Count >= 2); await service.StopAsync(token); Assert.That(mockEndpointSettingsStore.Deleted.Count, Is.EqualTo(2)); @@ -86,7 +97,7 @@ public async Task Should_set_the_default_for_settings_if_does_not_exist_already( }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointSettingsStore.Updated.Count >= 1); await service.StopAsync(token); Assert.That(mockEndpointSettingsStore.Updated.Count, Is.EqualTo(1)); @@ -106,11 +117,12 @@ public async Task Should_not_set_the_default_if_already_exists() var mockEndpointSettingsStore = new MockEndpointSettingsStore([ new EndpointSettings { Name = string.Empty, TrackInstances = expectedTrackInstancesInitialValue } ]); + var mockEndpointInstanceMonitoring = new MockEndpointInstanceMonitoring([]); var service = new HeartbeatEndpointSettingsSyncHostedService( new MockMonitoringDataStore( []), mockEndpointSettingsStore, - new MockEndpointInstanceMonitoring([]), + mockEndpointInstanceMonitoring, new Settings { TrackInstancesInitialValue = expectedTrackInstancesInitialValue }, fakeTimeProvider, NullLogger.Instance) { @@ -118,7 +130,7 @@ public async Task Should_not_set_the_default_if_already_exists() }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointInstanceMonitoring.GetEndpointsCallCount >= 1); await service.StopAsync(token); Assert.That(mockEndpointSettingsStore.Updated.Count, Is.EqualTo(0)); @@ -158,7 +170,7 @@ public async Task }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockMonitoringDataStore.Deleted.Count >= 2); await service.StopAsync(token); Assert.That(mockMonitoringDataStore.Deleted.Count, Is.EqualTo(2)); @@ -197,7 +209,7 @@ public async Task }; await service.StartAsync(token); - await Task.Delay(TimeSpan.FromSeconds(2), token); + await WaitUntilAsync(() => mockEndpointInstanceMonitoring.GetEndpointsCallCount >= 1); await service.StopAsync(token); Assert.That(mockMonitoringDataStore.Deleted.Count, Is.EqualTo(0)); @@ -220,7 +232,16 @@ public void DetectEndpointFromPersistentStore(EndpointDetails endpointDetails, b public Task EndpointDetected(EndpointDetails newEndpointDetails, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public EndpointsView[] GetEndpoints() => endpointsViews; + public EndpointsView[] GetEndpoints() + { + GetEndpointsCallCount++; + return endpointsViews; + } + + // GetEndpoints() is called by PurgeMonitoringDataThatDoesNotNeedToBeTracked, which runs + // at the end of each sync cycle. Waiting for this to be invoked gives a deterministic + // signal that a full sync cycle has completed, without relying on a fixed wall-clock delay. + public int GetEndpointsCallCount { get; private set; } public List GetKnownEndpoints() => throw new NotImplementedException(); From 563e5e1078a0e2423bc45e06522ecb3f404bf494 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 12 Aug 2026 16:35:22 -0700 Subject: [PATCH 2/2] fix formatting --- .../HeartbeatEndpointSettingsSyncHostedServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs index 37494dfe49..4ee83c9335 100644 --- a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs +++ b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs @@ -17,7 +17,7 @@ [TestFixture] public class HeartbeatEndpointSettingsSyncHostedServiceTests { - + static async Task WaitUntilAsync(Func condition, TimeSpan? timeout = null) { var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(10));