@@ -81,7 +81,7 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only
8181 feature_flag_enabled : bool = False ,
8282 feature_flag_selectors : Optional [List [SettingSelector ]] = None ,
8383 feature_flag_refresh_enabled : bool = False ,
84- ** kwargs
84+ ** kwargs ,
8585) -> "AzureAppConfigurationProvider" :
8686 """
8787 Loads configuration settings from Azure App Configuration into a Python application.
@@ -119,6 +119,9 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only
119119 :keyword replica_discovery_enabled: Optional flag to enable or disable the discovery of replica endpoints. Default
120120 is True.
121121 :paramtype replica_discovery_enabled: bool
122+ :keyword load_balancing_enabled: Optional flag to enable or disable the load balancing of replica endpoints. Default
123+ is False.
124+ :paramtype load_balancing_enabled: bool
122125 """
123126
124127
@@ -139,7 +142,7 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only
139142 feature_flag_enabled : bool = False ,
140143 feature_flag_selectors : Optional [List [SettingSelector ]] = None ,
141144 feature_flag_refresh_enabled : bool = False ,
142- ** kwargs
145+ ** kwargs ,
143146) -> "AzureAppConfigurationProvider" :
144147 """
145148 Loads configuration settings from Azure App Configuration into a Python application.
@@ -179,6 +182,9 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only
179182 :keyword replica_discovery_enabled: Optional flag to enable or disable the discovery of replica endpoints. Default
180183 is True.
181184 :paramtype replica_discovery_enabled: bool
185+ :keyword load_balancing_enabled: Optional flag to enable or disable the load balancing of replica endpoints. Default
186+ is False.
187+ :paramtype load_balancing_enabled: bool
182188 """
183189
184190
@@ -227,17 +233,9 @@ def load(*args, **kwargs) -> "AzureAppConfigurationProvider":
227233 )
228234
229235 provider = _buildprovider (connection_string , endpoint , credential , uses_key_vault = uses_key_vault , ** kwargs )
230- headers = _get_headers (
231- kwargs .pop ("headers" , {}),
232- "Startup" ,
233- provider ._replica_client_manager .get_client_count () - 1 , # pylint:disable=protected-access
234- provider ._feature_flag_enabled , # pylint:disable=protected-access
235- provider ._feature_filter_usage , # pylint:disable=protected-access
236- provider ._uses_key_vault , # pylint:disable=protected-access
237- )
238236
239237 try :
240- provider ._load_all (headers = headers ) # pylint:disable=protected-access
238+ provider ._load_all () # pylint:disable=protected-access
241239 except Exception as e :
242240 _delay_failure (start_time )
243241 raise e
@@ -253,7 +251,16 @@ def _delay_failure(start_time: datetime.datetime) -> None:
253251 time .sleep ((min_time - (current_time - start_time )).total_seconds ())
254252
255253
256- def _get_headers (headers , request_type , replica_count , uses_feature_flags , feature_filters_used , uses_key_vault ) -> str :
254+ def _update_correlation_context_header (
255+ headers ,
256+ request_type ,
257+ replica_count ,
258+ uses_feature_flags ,
259+ feature_filters_used ,
260+ uses_key_vault ,
261+ uses_load_balancing ,
262+ is_failover_request ,
263+ ) -> Dict [str , str ]:
257264 if os .environ .get (REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE , default = "" ).lower () == "true" :
258265 return headers
259266 correlation_context = "RequestType=" + request_type
@@ -291,6 +298,12 @@ def _get_headers(headers, request_type, replica_count, uses_feature_flags, featu
291298 if replica_count > 0 :
292299 correlation_context += ",ReplicaCount=" + str (replica_count )
293300
301+ if is_failover_request :
302+ correlation_context += ",Failover"
303+
304+ if uses_load_balancing :
305+ correlation_context += ",Features=LB"
306+
294307 headers ["Correlation-Context" ] = correlation_context
295308 return headers
296309
@@ -467,6 +480,8 @@ def __init__(self, **kwargs: Any) -> None:
467480 min_backoff : int = min (kwargs .pop ("min_backoff" , 30 ), interval )
468481 max_backoff : int = min (kwargs .pop ("max_backoff" , 600 ), interval )
469482
483+ self ._uses_load_balancing = kwargs .pop ("load_balancing_enabled" , False )
484+
470485 self ._replica_client_manager = ConfigurationClientManager (
471486 connection_string = kwargs .pop ("connection_string" , None ),
472487 endpoint = endpoint ,
@@ -477,7 +492,8 @@ def __init__(self, **kwargs: Any) -> None:
477492 replica_discovery_enabled = kwargs .pop ("replica_discovery_enabled" , True ),
478493 min_backoff_sec = min_backoff ,
479494 max_backoff_sec = max_backoff ,
480- ** kwargs
495+ load_balancing_enabled = self ._uses_load_balancing ,
496+ ** kwargs ,
481497 )
482498 self ._dict : Dict [str , Any ] = {}
483499 self ._secret_clients : Dict [str , SecretClient ] = {}
@@ -510,39 +526,44 @@ def __init__(self, **kwargs: Any) -> None:
510526 self ._update_lock = Lock ()
511527 self ._refresh_lock = Lock ()
512528
513- def refresh (self , ** kwargs ) -> None :
529+ def refresh (self , ** kwargs ) -> None : # pylint: disable=too-many-statements
514530 if not self ._refresh_on and not self ._feature_flag_refresh_enabled :
515- logging .debug ("Refresh called but no refresh enabled." )
531+ logger .debug ("Refresh called but no refresh enabled." )
516532 return
517533 if not self ._refresh_timer .needs_refresh ():
518- logging .debug ("Refresh called but refresh interval not elapsed." )
534+ logger .debug ("Refresh called but refresh interval not elapsed." )
519535 return
520536 if not self ._refresh_lock .acquire (blocking = False ): # pylint: disable= consider-using-with
521- logging .debug ("Refresh called but refresh already in progress." )
537+ logger .debug ("Refresh called but refresh already in progress." )
522538 return
523539 success = False
524540 need_refresh = False
525541 error_message = """
526542 Failed to refresh configuration settings from Azure App Configuration.
527543 """
528544 exception : Exception = RuntimeError (error_message )
545+ is_failover_request = False
529546 try :
530547 self ._replica_client_manager .refresh_clients ()
531- active_clients = self ._replica_client_manager .get_active_clients ()
548+ self ._replica_client_manager .find_active_clients ()
549+ replica_count = self ._replica_client_manager .get_client_count () - 1
550+
551+ while client := self ._replica_client_manager .get_next_active_client ():
552+ headers = _update_correlation_context_header (
553+ kwargs .pop ("headers" , {}),
554+ "Watch" ,
555+ replica_count ,
556+ self ._feature_flag_enabled ,
557+ self ._feature_filter_usage ,
558+ self ._uses_key_vault ,
559+ self ._uses_load_balancing ,
560+ is_failover_request ,
561+ )
532562
533- headers = _get_headers (
534- kwargs .pop ("headers" , {}),
535- "Watch" ,
536- self ._replica_client_manager .get_client_count () - 1 ,
537- self ._feature_flag_enabled ,
538- self ._feature_filter_usage ,
539- self ._uses_key_vault ,
540- )
541- for client in active_clients :
542563 try :
543564 if self ._refresh_on :
544565 need_refresh , self ._refresh_on , configuration_settings = client .refresh_configuration_settings (
545- self ._selects , self ._refresh_on , headers , ** kwargs
566+ self ._selects , self ._refresh_on , headers = headers , ** kwargs
546567 )
547568 configuration_settings_processed = {}
548569 for config in configuration_settings :
@@ -556,11 +577,13 @@ def refresh(self, **kwargs) -> None:
556577 if need_refresh :
557578 self ._dict = configuration_settings_processed
558579 if self ._feature_flag_refresh_enabled :
559- need_ff_refresh , self . _refresh_on_feature_flags , feature_flags , filters_used = (
580+ need_ff_refresh , refresh_on_feature_flags , feature_flags , filters_used = (
560581 client .refresh_feature_flags (
561- self ._refresh_on_feature_flags , self ._feature_flag_selectors , headers , ** kwargs
582+ self ._refresh_on_feature_flags , self ._feature_flag_selectors , headers = headers , ** kwargs
562583 )
563584 )
585+ if refresh_on_feature_flags :
586+ self ._refresh_on_feature_flags = refresh_on_feature_flags
564587 self ._feature_filter_usage = filters_used
565588
566589 if need_refresh or need_ff_refresh :
@@ -572,8 +595,9 @@ def refresh(self, **kwargs) -> None:
572595 break
573596 except AzureError as e :
574597 exception = e
598+ logger .debug ("Failed to refresh configurations from endpoint %s" , client .endpoint )
575599 self ._replica_client_manager .backoff (client )
576-
600+ is_failover_request = True
577601 if not success :
578602 self ._refresh_timer .backoff ()
579603 if self ._on_refresh_error :
@@ -586,21 +610,35 @@ def refresh(self, **kwargs) -> None:
586610 self ._refresh_lock .release ()
587611
588612 def _load_all (self , ** kwargs ):
589- active_clients = self ._replica_client_manager .get_active_clients ()
613+ self ._replica_client_manager .refresh_clients ()
614+ self ._replica_client_manager .find_active_clients ()
615+ is_failover_request = False
616+ replica_count = self ._replica_client_manager .get_client_count () - 1
590617
591- for client in active_clients :
618+ while client := self ._replica_client_manager .get_next_active_client ():
619+ headers = _update_correlation_context_header (
620+ kwargs .pop ("headers" , {}),
621+ "Startup" ,
622+ replica_count ,
623+ self ._feature_flag_enabled ,
624+ self ._feature_filter_usage ,
625+ self ._uses_key_vault ,
626+ self ._uses_load_balancing ,
627+ is_failover_request ,
628+ )
592629 try :
593630 configuration_settings , sentinel_keys = client .load_configuration_settings (
594- self ._selects , self ._refresh_on , ** kwargs
631+ self ._selects , self ._refresh_on , headers = headers , ** kwargs
595632 )
596633 configuration_settings_processed = {}
597634 for config in configuration_settings :
598635 key = self ._process_key_name (config )
599636 value = self ._process_key_value (config )
600637 configuration_settings_processed [key ] = value
638+
601639 if self ._feature_flag_enabled :
602640 feature_flags , feature_flag_sentinel_keys , used_filters = client .load_feature_flags (
603- self ._feature_flag_selectors , self ._feature_flag_refresh_enabled , ** kwargs
641+ self ._feature_flag_selectors , self ._feature_flag_refresh_enabled , headers = headers , ** kwargs
604642 )
605643 self ._feature_filter_usage = used_filters
606644 configuration_settings_processed [FEATURE_MANAGEMENT_KEY ] = {}
@@ -609,13 +647,12 @@ def _load_all(self, **kwargs):
609647 for (key , label ), etag in self ._refresh_on .items ():
610648 if not etag :
611649 try :
612- headers = kwargs .get ("headers" , {})
613650 sentinel = client .get_configuration_setting (key , label , headers = headers ) # type:ignore
614651 self ._refresh_on [(key , label )] = sentinel .etag # type:ignore
615652 except HttpResponseError as e :
616653 if e .status_code == 404 :
617654 # If the sentinel is not found a refresh should be triggered when it is created.
618- logging .debug (
655+ logger .debug (
619656 """
620657 WatchKey key: %s label %s was configured but not found. Refresh will be triggered
621658 if created.
@@ -631,7 +668,9 @@ def _load_all(self, **kwargs):
631668 self ._dict = configuration_settings_processed
632669 return
633670 except AzureError :
671+ logger .debug ("Failed to refresh configurations from endpoint %s" , client .endpoint )
634672 self ._replica_client_manager .backoff (client )
673+ is_failover_request = True
635674 raise RuntimeError (
636675 "Failed to load configuration settings. No Azure App Configuration stores successfully loaded from."
637676 )
0 commit comments