diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 975f1ab3..9b33c133 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -22,6 +22,7 @@ public class AzureAppConfigurationOptions { private const int MaxRetries = 2; private static readonly TimeSpan MaxRetryDelay = TimeSpan.FromMinutes(1); + private static readonly TimeSpan NetworkTimeout = TimeSpan.FromSeconds(10); private static readonly KeyValueSelector DefaultQuery = new KeyValueSelector { KeyFilter = KeyFilter.Any, LabelFilter = LabelFilter.Null }; private List _individualKvWatchers = new List(); @@ -529,6 +530,7 @@ private static ConfigurationClientOptions GetDefaultClientOptions() clientOptions.Retry.MaxRetries = MaxRetries; clientOptions.Retry.MaxDelay = MaxRetryDelay; clientOptions.Retry.Mode = RetryMode.Exponential; + clientOptions.Retry.NetworkTimeout = NetworkTimeout; clientOptions.AddPolicy(new UserAgentHeaderPolicy(), HttpPipelinePosition.PerCall); return clientOptions; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 28e2d507..a83c7413 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1232,6 +1232,11 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => private bool IsFailOverable(AggregateException ex) { + if (ex.InnerExceptions?.Any(e => e is TaskCanceledException) == true) + { + return true; + } + RequestFailedException rfe = ex.InnerExceptions?.LastOrDefault(e => e is RequestFailedException) as RequestFailedException; return rfe != null ? IsFailOverable(rfe) : false; diff --git a/tests/Tests.AzureAppConfiguration/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/FailoverTests.cs index 86ea96b9..105f267b 100644 --- a/tests/Tests.AzureAppConfiguration/FailoverTests.cs +++ b/tests/Tests.AzureAppConfiguration/FailoverTests.cs @@ -337,5 +337,84 @@ public void FailOverTests_GetNoDynamicClient() // Only contains the client that passed while constructing the ConfigurationClientManager Assert.Single(clients); } + + [Fact] + public void FailOverTests_NetworkTimeout() + { + // Arrange + IConfigurationRefresher refresher = null; + var mockResponse = new Mock(); + + var client1 = new ConfigurationClient(TestHelpers.CreateMockEndpointString(), + new ConfigurationClientOptions() + { + Retry = + { + NetworkTimeout = TimeSpan.FromTicks(1) + } + }); + + 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); + + ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, client1); + ConfigurationClientWrapper cw2 = new ConfigurationClientWrapper(TestHelpers.SecondaryConfigStoreEndpoint, mockClient2.Object); + + var clientList = new List() { cw1 }; + var autoFailoverList = new List() { cw2 }; + var configClientManager = new MockedConfigurationClientManager(clientList, autoFailoverList); + + // Make sure the provider fails over and will load correctly using the second client + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ClientManager = configClientManager; + options.Select("TestKey*"); + options.ConfigureRefresh(refreshOptions => + { + refreshOptions.Register("TestKey1", "label") + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + }) + .Build(); + + // Make sure the provider fails on startup and throws the expected exception due to startup timeout + Exception exception = Assert.Throws(() => + { + config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(TestHelpers.CreateMockEndpointString()); + options.Select("TestKey*"); + options.ConfigureRefresh(refreshOptions => + { + refreshOptions.Register("TestKey1", "label") + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + options.ConfigureStartupOptions(startup => + { + startup.Timeout = TimeSpan.FromSeconds(5); + }); + options.ConfigureClientOptions(clientOptions => + { + clientOptions.Retry.NetworkTimeout = TimeSpan.FromTicks(1); + }); + }) + .Build(); + }); + + // Make sure the startup exception is due to network timeout + // Aggregate exception is nested due to how provider stores all startup exceptions thrown + Assert.True(exception.InnerException is AggregateException ae && + ae.InnerException is AggregateException ae2 && + ae2.InnerExceptions.All(ex => ex is TaskCanceledException) && + ae2.InnerException is TaskCanceledException tce); + } } }