Skip to content

Commit ba88464

Browse files
Fix bug with endpoint failover (#686)
* update endpoint in do while * add test * in progress * update test, update logic to backoff using correct endpoint * make test more specific --------- Co-authored-by: AMER JUSUPOVIC <ajusupovic@microsoft.com> Co-authored-by: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
1 parent ae71811 commit ba88464

File tree

2 files changed

+85
-1
lines changed

2 files changed

+85
-1
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1214,7 +1214,7 @@ private async Task<T> ExecuteWithFailOverPolicyAsync<T>(
12141214

12151215
do
12161216
{
1217-
UpdateClientBackoffStatus(previousEndpoint, success);
1217+
UpdateClientBackoffStatus(_configClientManager.GetEndpointForClient(currentClient), success);
12181218

12191219
clientEnumerator.MoveNext();
12201220

tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,5 +415,89 @@ ae.InnerException is AggregateException ae2 &&
415415
ae2.InnerExceptions.All(ex => ex is TaskCanceledException) &&
416416
ae2.InnerException is TaskCanceledException tce);
417417
}
418+
419+
[Fact]
420+
public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException()
421+
{
422+
IConfigurationRefresher refresher = null;
423+
var mockResponse = new Mock<Response>();
424+
425+
// Setup first client - succeeds on startup, fails with 404 (non-failoverable) on first refresh
426+
var mockClient1 = new Mock<ConfigurationClient>();
427+
mockClient1.SetupSequence(c => c.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()))
428+
.Returns(new MockAsyncPageable(Enumerable.Empty<ConfigurationSetting>().ToList()))
429+
.Throws(new RequestFailedException(412, "Request failed."))
430+
.Throws(new RequestFailedException(412, "Request failed."));
431+
mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
432+
.Returns(Task.FromResult(Response.FromValue<ConfigurationSetting>(kv, mockResponse.Object)))
433+
.Throws(new RequestFailedException(412, "Request failed."))
434+
.Throws(new RequestFailedException(412, "Request failed."));
435+
mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
436+
.Throws(new RequestFailedException(412, "Request failed."))
437+
.Throws(new RequestFailedException(412, "Request failed."));
438+
mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true);
439+
440+
// Setup second client - succeeds on startup, should not be called during refresh
441+
var mockClient2 = new Mock<ConfigurationClient>();
442+
mockClient2.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()))
443+
.Returns(new MockAsyncPageable(Enumerable.Empty<ConfigurationSetting>().ToList()));
444+
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
445+
.Returns(Task.FromResult(Response.FromValue<ConfigurationSetting>(kv, mockResponse.Object)));
446+
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
447+
.Returns(Task.FromResult(Response.FromValue<ConfigurationSetting>(kv, mockResponse.Object)));
448+
mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true);
449+
450+
ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object);
451+
ConfigurationClientWrapper cw2 = new ConfigurationClientWrapper(TestHelpers.SecondaryConfigStoreEndpoint, mockClient2.Object);
452+
453+
var clientList = new List<ConfigurationClientWrapper>() { cw1, cw2 };
454+
var configClientManager = new ConfigurationClientManager(clientList);
455+
456+
// Verify 2 clients are available
457+
Assert.Equal(2, configClientManager.GetClients().Count());
458+
459+
// Act & Assert - Build configuration successfully with both clients
460+
var config = new ConfigurationBuilder()
461+
.AddAzureAppConfiguration(options =>
462+
{
463+
options.ClientManager = configClientManager;
464+
options.Select("TestKey*");
465+
options.ConfigureRefresh(refreshOptions =>
466+
{
467+
refreshOptions.Register("TestKey1", "label")
468+
.SetRefreshInterval(TimeSpan.FromSeconds(1));
469+
});
470+
471+
options.ReplicaDiscoveryEnabled = false;
472+
refresher = options.GetRefresher();
473+
}).Build();
474+
475+
// First refresh - should call client 1 and fail with non-failoverable exception
476+
// This should cause all clients to be backed off
477+
await Task.Delay(1500);
478+
await refresher.TryRefreshAsync();
479+
480+
// Verify that client 1 was called during the first refresh
481+
mockClient1.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
482+
mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
483+
mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
484+
485+
// Verify that client 2 was not called during the first refresh
486+
mockClient2.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()), Times.Never);
487+
mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
488+
mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
489+
490+
// Second refresh - no clients should be called as all are backed off
491+
await Task.Delay(1500);
492+
await refresher.TryRefreshAsync();
493+
494+
// Verify that no additional calls were made to any client during the second refresh
495+
mockClient1.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
496+
mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
497+
mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
498+
mockClient2.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()), Times.Never);
499+
mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
500+
mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
501+
}
418502
}
419503
}

0 commit comments

Comments
 (0)