Skip to content

Commit d90e4e4

Browse files
mrm9084avaniguptaalbertofori
authored
App Configuration Python Provider - Load Balancing (Azure#37692)
* load balancing * fix rotation bug + added tests * code cleanup/docstrings * Added Changelog, fixed bug where refreshing the clients resulted in a reshuffle even if nothing changed. * adding missing telemetry * Code Review comments * formatting * PR comments * fixed logger, added client fail log * fixing extra _ * fixing failover? * Apply suggestions from code review Co-authored-by: Avani Gupta <[email protected]> * _load_balancing_enabled * fixing other instances * fixing _ * review comments * Update _client_manager.py * Update _async_client_manager.py * reworked logic * fixed tests and formatting * fixing checking for clients * Update _azureappconfigurationprovider.py * Update _azureappconfigurationproviderasync.py * Update _azureappconfigurationprovider.py * Update _azureappconfigurationprovider.py * Async changes to match sync * fixing shuffle * formatting * Update sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py Co-authored-by: Avani Gupta <[email protected]> * Update sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py Co-authored-by: Avani Gupta <[email protected]> * get_next_active_client * updating async with review changes * move outside if * fixing refresh * Update sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py Co-authored-by: Albert Ofori <[email protected]> * always reshuffling * review comments * format fix --------- Co-authored-by: Avani Gupta <[email protected]> Co-authored-by: Albert Ofori <[email protected]>
1 parent da9b5ff commit d90e4e4

10 files changed

+787
-341
lines changed

sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
### Breaking Changes
88

9+
### Features Added
10+
11+
* Added support for load balancing between replicas.
12+
913
### Bugs Fixed
1014

1115
### Other Changes

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)