Skip to content

Commit 6ce60b9

Browse files
Handle exceptions thrown during Key Vault reference resolution during startup (#518)
* treat keyvaultreferenceexception as failoverable when inner exception is failoverable * check for aggregateexception, make isfailoverable for keyvault exception
1 parent 5c9155b commit 6ce60b9

File tree

2 files changed

+87
-0
lines changed

2 files changed

+87
-0
lines changed

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,17 @@ private async Task<bool> TryInitializeAsync(IEnumerable<ConfigurationClient> cli
630630

631631
throw;
632632
}
633+
catch (KeyVaultReferenceException exception)
634+
{
635+
if (IsFailOverable(exception))
636+
{
637+
startupExceptions.Add(exception);
638+
639+
return false;
640+
}
641+
642+
throw;
643+
}
633644
catch (AggregateException exception)
634645
{
635646
if (exception.InnerExceptions?.Any(e => e is OperationCanceledException) ?? false)
@@ -980,6 +991,15 @@ private async Task<T> ExecuteWithFailOverPolicyAsync<T>(
980991
throw;
981992
}
982993
}
994+
catch (KeyVaultReferenceException kvre)
995+
{
996+
if (!IsFailOverable(kvre) || !clientEnumerator.MoveNext())
997+
{
998+
backoffAllClients = true;
999+
1000+
throw;
1001+
}
1002+
}
9831003
catch (AggregateException ae)
9841004
{
9851005
if (!IsFailOverable(ae) || !clientEnumerator.MoveNext())
@@ -1066,6 +1086,20 @@ innerException is SocketException ||
10661086
innerException is IOException;
10671087
}
10681088

1089+
private bool IsFailOverable(KeyVaultReferenceException kvre)
1090+
{
1091+
if (kvre.InnerException is RequestFailedException rfe && IsFailOverable(rfe))
1092+
{
1093+
return true;
1094+
}
1095+
else if (kvre.InnerException is AggregateException ae && IsFailOverable(ae))
1096+
{
1097+
return true;
1098+
}
1099+
1100+
return false;
1101+
}
1102+
10691103
private async Task<Dictionary<string, ConfigurationSetting>> MapConfigurationSettings(Dictionary<string, ConfigurationSetting> data)
10701104
{
10711105
Dictionary<string, ConfigurationSetting> mappedData = new Dictionary<string, ConfigurationSetting>(StringComparer.OrdinalIgnoreCase);

tests/Tests.AzureAppConfiguration/FailoverTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,5 +257,58 @@ public void FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
257257
// The client enumerator should return 2 clients for the third time.
258258
Assert.Equal(2, configClientManager.GetAvailableClients(DateTimeOffset.UtcNow).Count());
259259
}
260+
261+
[Fact]
262+
public void FailOverTests_FailOverOnKeyVaultReferenceException()
263+
{
264+
// Arrange
265+
IConfigurationRefresher refresher = null;
266+
var mockResponse = new Mock<Response>();
267+
268+
var mockClient1 = new Mock<ConfigurationClient>();
269+
mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()))
270+
.Throws(new KeyVaultReferenceException("Key vault reference failed.", new RequestFailedException(503, "Request failed.")));
271+
mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
272+
.Throws(new KeyVaultReferenceException("Key vault reference failed.", new RequestFailedException(503, "Request failed.")));
273+
mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
274+
.Throws(new KeyVaultReferenceException("Key vault reference failed.", new RequestFailedException(503, "Request failed.")));
275+
mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true);
276+
277+
var mockClient2 = new Mock<ConfigurationClient>();
278+
mockClient2.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()))
279+
.Returns(new MockAsyncPageable(Enumerable.Empty<ConfigurationSetting>().ToList()));
280+
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
281+
.Returns(Task.FromResult(Response.FromValue<ConfigurationSetting>(kv, mockResponse.Object)));
282+
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
283+
.Returns(Task.FromResult(Response.FromValue<ConfigurationSetting>(kv, mockResponse.Object)));
284+
mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true);
285+
286+
ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object);
287+
ConfigurationClientWrapper cw2 = new ConfigurationClientWrapper(TestHelpers.SecondaryConfigStoreEndpoint, mockClient2.Object);
288+
289+
var clientList = new List<ConfigurationClientWrapper>() { cw1, cw2 };
290+
var configClientManager = new ConfigurationClientManager(clientList);
291+
292+
var config = new ConfigurationBuilder()
293+
.AddAzureAppConfiguration(options =>
294+
{
295+
options.ClientManager = configClientManager;
296+
options.Select("TestKey*");
297+
options.ConfigureRefresh(refreshOptions =>
298+
{
299+
refreshOptions.Register("TestKey1", "label")
300+
.SetCacheExpiration(TimeSpan.FromSeconds(1));
301+
});
302+
303+
refresher = options.GetRefresher();
304+
})
305+
.Build();
306+
307+
// The client enumerator should return just 1 client.
308+
Assert.Single(configClientManager.GetAvailableClients(DateTimeOffset.UtcNow));
309+
310+
// The build should be successful since one client was backed off and it failed over to the second client.
311+
Assert.Equal("TestValue1", config["TestKey1"]);
312+
}
260313
}
261314
}

0 commit comments

Comments
 (0)