@@ -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