diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index d7d629f8..9d61faca 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -118,6 +118,13 @@ public AzureAppConfigurationProvider(IConfigurationClientManager configClientMan if (options.RegisterAllEnabled) { + if (options.KvCollectionRefreshInterval <= TimeSpan.Zero) + { + throw new ArgumentException( + $"{nameof(options.KvCollectionRefreshInterval)} must be greater than zero seconds when using RegisterAll for refresh", + nameof(options)); + } + MinRefreshInterval = TimeSpan.FromTicks(Math.Min(minWatcherRefreshInterval.Ticks, options.KvCollectionRefreshInterval.Ticks)); } else if (hasWatchers) @@ -206,7 +213,7 @@ public async Task RefreshAsync(CancellationToken cancellationToken) var utcNow = DateTimeOffset.UtcNow; IEnumerable refreshableIndividualKvWatchers = _options.IndividualKvWatchers.Where(kvWatcher => utcNow >= kvWatcher.NextRefreshTime); IEnumerable refreshableFfWatchers = _options.FeatureFlagWatchers.Where(ffWatcher => utcNow >= ffWatcher.NextRefreshTime); - bool isRefreshDue = utcNow >= _nextCollectionRefreshTime; + bool isRefreshDue = _options.RegisterAllEnabled && utcNow >= _nextCollectionRefreshTime; // Skip refresh if mappedData is loaded, but none of the watchers or adapters are refreshable. if (_mappedData != null && @@ -412,7 +419,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => } } - if (isRefreshDue) + if (_options.RegisterAllEnabled && isRefreshDue) { _nextCollectionRefreshTime = DateTimeOffset.UtcNow.Add(_options.KvCollectionRefreshInterval); } diff --git a/tests/Tests.AzureAppConfiguration/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/RefreshTests.cs index af6dd11d..e64b5184 100644 --- a/tests/Tests.AzureAppConfiguration/RefreshTests.cs +++ b/tests/Tests.AzureAppConfiguration/RefreshTests.cs @@ -212,7 +212,7 @@ public async Task RefreshTests_RefreshIsNotSkippedIfCacheIsExpired() _kvCollection[0] = TestHelpers.ChangeValue(FirstKeyValue, "newValue"); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -247,7 +247,7 @@ public async Task RefreshTests_RefreshAllFalseDoesNotUpdateEntireConfiguration() _kvCollection = _kvCollection.Select(kv => TestHelpers.ChangeValue(kv, "newValue")).ToList(); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -284,7 +284,7 @@ public async Task RefreshTests_RefreshAllTrueUpdatesEntireConfiguration() _kvCollection = _kvCollection.Select(kv => TestHelpers.ChangeValue(kv, "newValue")).ToList(); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -356,7 +356,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o keyValueCollection.Remove(keyValueCollection.FirstOrDefault(s => s.Key == "TestKey3" && s.Label == "label")); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -430,7 +430,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o keyValueCollection.Remove(keyValueCollection.FirstOrDefault(s => s.Key == "TestKey3" && s.Label == "label")); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -443,32 +443,33 @@ Response GetIfChanged(ConfigurationSetting setting, bool o } [Fact] - public async void RefreshTests_SingleServerCallOnSimultaneousMultipleRefresh() + public async Task RefreshTests_SingleServerCallOnSimultaneousMultipleRefresh() { var keyValueCollection = new List(_kvCollection); var requestCount = 0; var mockResponse = new Mock(); var mockClient = new Mock(MockBehavior.Strict); + // Define delay for async operations + var operationDelay = TimeSpan.FromSeconds(6); + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) .Returns(() => { requestCount++; - Thread.Sleep(6000); - var copy = new List(); foreach (var setting in keyValueCollection) { copy.Add(TestHelpers.CloneSetting(setting)); }; - return new MockAsyncPageable(copy); + return new MockAsyncPageable(copy, operationDelay); }); - Response GetIfChanged(ConfigurationSetting setting, bool onlyIfChanged, CancellationToken cancellationToken) + async Task> GetIfChanged(ConfigurationSetting setting, bool onlyIfChanged, CancellationToken cancellationToken) { requestCount++; - Thread.Sleep(6000); + await Task.Delay(operationDelay, cancellationToken); var newSetting = keyValueCollection.FirstOrDefault(s => s.Key == setting.Key && s.Label == setting.Label); var unchanged = (newSetting.Key == setting.Key && newSetting.Label == setting.Label && newSetting.Value == setting.Value); @@ -477,7 +478,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o } mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync((Func>)GetIfChanged); + .Returns((Func>>)GetIfChanged); IConfigurationRefresher refresher = null; @@ -512,7 +513,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o } [Fact] - public void RefreshTests_RefreshAsyncThrowsOnRequestFailedException() + public async Task RefreshTests_RefreshAsyncThrowsOnRequestFailedException() { IConfigurationRefresher refresher = null; var mockClient = GetMockConfigurationClient(); @@ -539,7 +540,7 @@ public void RefreshTests_RefreshAsyncThrowsOnRequestFailedException() .Throws(new RequestFailedException("Request failed.")); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); Action action = () => refresher.RefreshAsync().Wait(); Assert.Throws(action); @@ -575,7 +576,7 @@ public async Task RefreshTests_TryRefreshAsyncReturnsFalseOnRequestFailedExcepti .Throws(new RequestFailedException("Request failed.")); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); bool result = await refresher.TryRefreshAsync(); Assert.False(result); @@ -608,7 +609,7 @@ public async Task RefreshTests_TryRefreshAsyncUpdatesConfigurationAndReturnsTrue _kvCollection[0] = TestHelpers.ChangeValue(_kvCollection[0], "newValue"); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); bool result = await refresher.TryRefreshAsync(); Assert.True(result); @@ -651,13 +652,13 @@ public async Task RefreshTests_TryRefreshAsyncReturnsFalseForAuthenticationFaile FirstKeyValue.Value = "newValue"; // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); // First call to GetConfigurationSettingAsync does not throw Assert.True(await refresher.TryRefreshAsync()); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); // Second call to GetConfigurationSettingAsync throws KeyVaultReferenceException Assert.False(await refresher.TryRefreshAsync()); @@ -704,7 +705,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o _kvCollection[0] = TestHelpers.ChangeValue(_kvCollection[0], "newValue"); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await Assert.ThrowsAsync(async () => await refresher.RefreshAsync() @@ -748,7 +749,7 @@ public async Task RefreshTests_UpdatesAllSettingsIfInitialLoadFails() Assert.Null(configuration["TestKey3"]); // Make sure MinBackoffDuration has ended - Thread.Sleep(100); + await Task.Delay(100); // Act await Assert.ThrowsAsync(async () => @@ -763,7 +764,7 @@ await Assert.ThrowsAsync(async () => Assert.Null(configuration["TestKey3"]); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -825,7 +826,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o keyValueCollection = keyValueCollection.Select(kv => TestHelpers.ChangeValue(kv, "newValue")).ToList(); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); bool firstRefreshResult = await refresher.TryRefreshAsync(); Assert.False(firstRefreshResult); @@ -835,7 +836,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o Assert.Equal("TestValue3", config["TestKey3"]); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); bool secondRefreshResult = await refresher.TryRefreshAsync(); Assert.True(secondRefreshResult); @@ -876,7 +877,7 @@ public async Task RefreshTests_RefreshAllTrueForOverwrittenSentinelUpdatesEntire _kvCollection = _kvCollection.Select(kv => TestHelpers.ChangeValue(kv, "newValue")).ToList(); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -917,7 +918,7 @@ public async Task RefreshTests_RefreshAllFalseForOverwrittenSentinelUpdatesConfi _kvCollection[_kvCollection.IndexOf(refreshRegisteredSetting)] = TestHelpers.ChangeValue(refreshRegisteredSetting, "UpdatedValueForLabel1"); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -959,7 +960,7 @@ public async Task RefreshTests_RefreshRegisteredKvOverwritesSelectedKv() _kvCollection[_kvCollection.IndexOf(refreshAllRegisteredSetting)] = TestHelpers.ChangeValue(refreshAllRegisteredSetting, "UpdatedValueForLabel1"); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -1020,7 +1021,7 @@ public void RefreshTests_ConfigureRefreshThrowsOnNoRegistration() } [Fact] - public void RefreshTests_RefreshIsCancelled() + public async Task RefreshTests_RefreshIsCancelled() { IConfigurationRefresher refresher = null; var mockClient = GetMockConfigurationClient(); @@ -1043,7 +1044,7 @@ public void RefreshTests_RefreshIsCancelled() FirstKeyValue.Value = "newValue1"; // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); using var cancellationSource = new CancellationTokenSource(); cancellationSource.Cancel(); @@ -1087,7 +1088,7 @@ public async Task RefreshTests_SelectedKeysRefreshWithRegisterAll() _kvCollection[2].Value = "newValue3"; // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -1097,7 +1098,7 @@ public async Task RefreshTests_SelectedKeysRefreshWithRegisterAll() _kvCollection.RemoveAt(2); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -1198,7 +1199,7 @@ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct) eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); @@ -1209,7 +1210,7 @@ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct) featureFlags.RemoveAt(0); // Wait for the cache to expire - Thread.Sleep(1500); + await Task.Delay(1500); await refresher.RefreshAsync(); diff --git a/tests/Tests.AzureAppConfiguration/TestHelper.cs b/tests/Tests.AzureAppConfiguration/TestHelper.cs index ca929efc..9fd3f388 100644 --- a/tests/Tests.AzureAppConfiguration/TestHelper.cs +++ b/tests/Tests.AzureAppConfiguration/TestHelper.cs @@ -164,8 +164,9 @@ class MockAsyncPageable : AsyncPageable { private readonly List _collection = new List(); private int _status; + private readonly TimeSpan? _delay; - public MockAsyncPageable(List collection) + public MockAsyncPageable(List collection, TimeSpan? delay = null) { foreach (ConfigurationSetting setting in collection) { @@ -177,6 +178,7 @@ public MockAsyncPageable(List collection) } _status = 200; + _delay = delay; } public void UpdateCollection(List newCollection) @@ -207,10 +209,13 @@ public void UpdateCollection(List newCollection) } } -#pragma warning disable 1998 - public async override IAsyncEnumerable> AsPages(string continuationToken = null, int? pageSizeHint = null) -#pragma warning restore 1998 + public override async IAsyncEnumerable> AsPages(string continuationToken = null, int? pageSizeHint = null) { + if (_delay.HasValue) + { + await Task.Delay(_delay.Value); + } + yield return Page.FromValues(_collection, null, new MockResponse(_status)); } }