From bba05b0e6ebeceb66fc1b5d4cd08eaf85e8fe61e Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 24 Jun 2025 10:01:58 -0700 Subject: [PATCH 1/5] update endpoint in do while --- .../AzureAppConfigurationProvider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 6b100f8da..42ff854d1 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1212,7 +1212,9 @@ private async Task ExecuteWithFailOverPolicyAsync( do { - UpdateClientBackoffStatus(previousEndpoint, success); + Uri endpointToBackoff = _configClientManager.GetEndpointForClient(clientEnumerator.Current); + + UpdateClientBackoffStatus(endpointToBackoff, success); clientEnumerator.MoveNext(); From 4d4be497265e0687603cdd96636d5fe5984cc990 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 24 Jun 2025 12:37:22 -0700 Subject: [PATCH 2/5] add test --- .../Unit/FailoverTests.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs index 810f9400e..12a1621d4 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs @@ -415,5 +415,85 @@ ae.InnerException is AggregateException ae2 && ae2.InnerExceptions.All(ex => ex is TaskCanceledException) && ae2.InnerException is TaskCanceledException tce); } + + [Fact] + public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException() + { + // Arrange + IConfigurationRefresher refresher = null; + var mockResponse = new Mock(); + var tertiaryConfigStoreEndpoint = new Uri("https://azure---eus.azconfig.io"); // Setup first client - succeeds on startup, fails with 404 (non-failoverable) on first refresh + var mockClient1 = new Mock(); + mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(Enumerable.Empty().ToList())); + mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new RequestFailedException(404, "Not found.")); + mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); + mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true); + + // Setup second client - succeeds on startup, should not be called during refresh + var mockClient2 = new Mock(); + mockClient2.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(Enumerable.Empty().ToList())); + mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); + mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); + mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true); + + // Setup third client - succeeds on startup, should not be called during refresh + var mockClient3 = new Mock(); + mockClient3.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(Enumerable.Empty().ToList())); + mockClient3.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); + mockClient3.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); + mockClient3.Setup(c => c.Equals(mockClient3)).Returns(true); + + ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object); + ConfigurationClientWrapper cw2 = new ConfigurationClientWrapper(TestHelpers.SecondaryConfigStoreEndpoint, mockClient2.Object); + ConfigurationClientWrapper cw3 = new ConfigurationClientWrapper(tertiaryConfigStoreEndpoint, mockClient3.Object); + + var clientList = new List() { cw1, cw2, cw3 }; + var configClientManager = new ConfigurationClientManager(clientList); + + // Verify all 3 clients are available + Assert.Equal(3, configClientManager.GetClients().Count()); + + // Act & Assert - Build configuration successfully with all 3 clients + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ClientManager = configClientManager; + options.Select("TestKey*"); + options.ConfigureRefresh(refreshOptions => + { + refreshOptions.Register("TestKey1", "label") + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + options.ReplicaDiscoveryEnabled = false; + refresher = options.GetRefresher(); + }).Build(); + + // First refresh - should call client 1 and fail with non-failoverable exception + // This should cause all clients to be backed off + await refresher.RefreshAsync(); // Verify that client 1 was called during the first refresh + mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + + // Verify that clients 2 and 3 were not called during the first refresh + mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + mockClient3.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + + // Second refresh - no clients should be called as all are backed off + await refresher.RefreshAsync(); + + // Verify that no additional calls were made to any client during the second refresh + mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + mockClient3.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } } } From d38fe14a6ca6b35221a89ee4711c597fcc482515 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 26 Jun 2025 12:48:36 -0700 Subject: [PATCH 3/5] in progress --- .../AzureAppConfigurationProvider.cs | 6 ++- .../Unit/FailoverTests.cs | 52 +++++++++---------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 42ff854d1..3be270ba7 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1212,9 +1212,11 @@ private async Task ExecuteWithFailOverPolicyAsync( do { - Uri endpointToBackoff = _configClientManager.GetEndpointForClient(clientEnumerator.Current); + //Uri endpointToBackoff = _configClientManager.GetEndpointForClient(clientEnumerator.Current); - UpdateClientBackoffStatus(endpointToBackoff, success); + //UpdateClientBackoffStatus(endpointToBackoff, success); + + UpdateClientBackoffStatus(previousEndpoint, success); clientEnumerator.MoveNext(); diff --git a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs index 12a1621d4..c72233752 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs @@ -419,17 +419,23 @@ ae.InnerException is AggregateException ae2 && [Fact] public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException() { - // Arrange IConfigurationRefresher refresher = null; var mockResponse = new Mock(); - var tertiaryConfigStoreEndpoint = new Uri("https://azure---eus.azconfig.io"); // Setup first client - succeeds on startup, fails with 404 (non-failoverable) on first refresh + + // Setup first client - succeeds on startup, fails with 404 (non-failoverable) on first refresh var mockClient1 = new Mock(); - mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) - .Returns(new MockAsyncPageable(Enumerable.Empty().ToList())); - mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(new RequestFailedException(404, "Not found.")); - mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); + mockClient1.SetupSequence(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(Enumerable.Empty().ToList())) + .Throws(new RequestFailedException(412, "Request failed.")) + .Throws(new RequestFailedException(412, "Request failed.")); + mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))) + .Throws(new RequestFailedException(412, "Request failed.")) + .Throws(new RequestFailedException(412, "Request failed.")); + mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + //.Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))) + .Throws(new RequestFailedException(412, "Request failed.")) + .Throws(new RequestFailedException(412, "Request failed.")); mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true); // Setup second client - succeeds on startup, should not be called during refresh @@ -442,27 +448,16 @@ public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true); - // Setup third client - succeeds on startup, should not be called during refresh - var mockClient3 = new Mock(); - mockClient3.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) - .Returns(new MockAsyncPageable(Enumerable.Empty().ToList())); - mockClient3.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); - mockClient3.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); - mockClient3.Setup(c => c.Equals(mockClient3)).Returns(true); - ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object); ConfigurationClientWrapper cw2 = new ConfigurationClientWrapper(TestHelpers.SecondaryConfigStoreEndpoint, mockClient2.Object); - ConfigurationClientWrapper cw3 = new ConfigurationClientWrapper(tertiaryConfigStoreEndpoint, mockClient3.Object); - var clientList = new List() { cw1, cw2, cw3 }; + var clientList = new List() { cw1, cw2 }; var configClientManager = new ConfigurationClientManager(clientList); - // Verify all 3 clients are available - Assert.Equal(3, configClientManager.GetClients().Count()); + // Verify 2 clients are available + Assert.Equal(2, configClientManager.GetClients().Count()); - // Act & Assert - Build configuration successfully with all 3 clients + // Act & Assert - Build configuration successfully with both clients var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { @@ -480,20 +475,21 @@ public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException // First refresh - should call client 1 and fail with non-failoverable exception // This should cause all clients to be backed off - await refresher.RefreshAsync(); // Verify that client 1 was called during the first refresh + await Task.Delay(1500); + await refresher.TryRefreshAsync(); + // Verify that client 1 was called during the first refresh mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); - // Verify that clients 2 and 3 were not called during the first refresh + // Verify that client 2 was not called during the first refresh mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - mockClient3.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); // Second refresh - no clients should be called as all are backed off - await refresher.RefreshAsync(); + await Task.Delay(1500); + await refresher.TryRefreshAsync(); // Verify that no additional calls were made to any client during the second refresh mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - mockClient3.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } } } From 82edede12a493ea13f2a48ea698667d1814cb8dc Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 26 Jun 2025 13:21:16 -0700 Subject: [PATCH 4/5] update test, update logic to backoff using correct endpoint --- .../AzureAppConfigurationProvider.cs | 6 +----- tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs | 9 +++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 3be270ba7..0f3893b53 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1212,11 +1212,7 @@ private async Task ExecuteWithFailOverPolicyAsync( do { - //Uri endpointToBackoff = _configClientManager.GetEndpointForClient(clientEnumerator.Current); - - //UpdateClientBackoffStatus(endpointToBackoff, success); - - UpdateClientBackoffStatus(previousEndpoint, success); + UpdateClientBackoffStatus(_configClientManager.GetEndpointForClient(currentClient), success); clientEnumerator.MoveNext(); diff --git a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs index c72233752..081794422 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs @@ -433,7 +433,6 @@ public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException .Throws(new RequestFailedException(412, "Request failed.")) .Throws(new RequestFailedException(412, "Request failed.")); mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) - //.Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))) .Throws(new RequestFailedException(412, "Request failed.")) .Throws(new RequestFailedException(412, "Request failed.")); mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true); @@ -478,18 +477,20 @@ public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException await Task.Delay(1500); await refresher.TryRefreshAsync(); // Verify that client 1 was called during the first refresh - mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); // Verify that client 2 was not called during the first refresh - mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); // Second refresh - no clients should be called as all are backed off await Task.Delay(1500); await refresher.TryRefreshAsync(); // Verify that no additional calls were made to any client during the second refresh - mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient2.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Never); mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } } } From 0103c1dc2b7055b79418a77b6a23f2836db760cd Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 26 Jun 2025 15:01:03 -0700 Subject: [PATCH 5/5] make test more specific --- tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs index 081794422..57735f379 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs @@ -476,10 +476,15 @@ public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException // This should cause all clients to be backed off await Task.Delay(1500); await refresher.TryRefreshAsync(); + // Verify that client 1 was called during the first refresh + mockClient1.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); // Verify that client 2 was not called during the first refresh + mockClient2.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Never); + mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); // Second refresh - no clients should be called as all are backed off @@ -487,6 +492,8 @@ public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException await refresher.TryRefreshAsync(); // Verify that no additional calls were made to any client during the second refresh + mockClient1.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); mockClient2.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Never); mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);