diff --git a/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs b/src/ServiceControl.UnitTests/Monitoring/HeartbeatEndpointSettingsSyncHostedServiceTests.cs index 2f2252afe2..29acd797af 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();