diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md index 54481723b4d3..74d1ec6ae129 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md @@ -8,6 +8,9 @@ ### Bugs Fixed +- Fixed a resource leak where replica clients that were no longer part of the auto-failover set were not closed during client refresh. +- Fixed auto-failover replica discovery so that a DNS SRV lookup timeout is distinguished from an empty replica list. A timeout now correctly triggers the longer fallback refresh interval, while an empty result refreshes at the normal interval. + ### Other Changes - Bumped minimum dependency on `azure-core` to `>=1.31.0`. diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index a3f8f826679c..6977edbf2301 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -34,7 +34,6 @@ from ._user_agent import USER_AGENT from ._utils import get_startup_backoff -JSON = Mapping[str, Any] logger = logging.getLogger(__name__) @@ -79,7 +78,7 @@ def __init__(self, **kwargs: Any) -> None: self._startup_timeout: int = kwargs.pop("startup_timeout", DEFAULT_STARTUP_TIMEOUT) self._replica_client_manager = ConfigurationClientManager( - connection_string=kwargs.pop("connection_string"), + connection_string=kwargs.pop("connection_string", None), endpoint=kwargs.pop("endpoint"), credential=kwargs.pop("credential", None), user_agent=user_agent, @@ -96,7 +95,19 @@ def __init__(self, **kwargs: Any) -> None: self._on_refresh_error: Optional[Callable[[Exception], None]] = kwargs.pop("on_refresh_error", None) self._configuration_mapper: Optional[Callable] = kwargs.pop("configuration_mapper", None) - def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_failover_request: bool, **kwargs): + def _attempt_refresh( + self, client: ConfigurationClient, replica_count: int, is_failover_request: bool, **kwargs + ) -> None: + """ + Attempts to refresh configuration settings and feature flags using a single client. + + :param client: The configuration client to attempt the refresh against. + :type client: ~azure.appconfiguration.provider.ConfigurationClient + :param replica_count: The number of replica clients available, used for correlation telemetry. + :type replica_count: int + :param is_failover_request: Whether this attempt is a failover from a previously failed client. + :type is_failover_request: bool + """ settings_refreshed = False headers = self._update_correlation_context_header( kwargs.pop("headers", {}), diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index e5b6240d74e6..a97a7081d4e3 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -70,11 +70,13 @@ def is_json_content_type(content_type: str) -> bool: def _build_watched_setting(setting: Union[str, Tuple[str, str]]) -> Tuple[str, str]: - try: - key, label = setting # type:ignore - except (IndexError, ValueError): - key = str(setting) # Ensure key is a string - label = NULL_CHAR + if isinstance(setting, str): + key, label = setting, NULL_CHAR + else: + try: + key, label = setting + except (TypeError, ValueError): + key, label = str(setting), NULL_CHAR if "*" in key or "*" in label: raise ValueError("Wildcard key or label filters are not supported for refresh.") return key, label @@ -179,7 +181,7 @@ def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str seed=123abc\ndefault_when_enabled=Control\npercentiles=0,Control,20;20,Test,100\nvariants=Control,standard;Test,special # pylint:disable=line-too-long :param Dict[str, JSON] feature_flag_value: The feature to generate an allocation ID for. - :rtype: str + :rtype: Optional[str] :return: The allocation ID. """ @@ -264,17 +266,20 @@ def __getitem__(self, key: str) -> Any: return self._dict[key] def __iter__(self) -> Iterator[str]: - return self._dict.__iter__() + with self._update_lock: + return self._dict.__iter__() def __len__(self) -> int: - return len(self._dict) + with self._update_lock: + return len(self._dict) def __contains__(self, __x: object) -> bool: # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype """ Returns True if the configuration settings contains the specified key. """ - return self._dict.__contains__(__x) + with self._update_lock: + return self._dict.__contains__(__x) def keys(self) -> KeysView[str]: """ @@ -303,11 +308,11 @@ def values(self) -> ValuesView[Union[str, Mapping[str, Any]]]: resolved. :return: A list of values loaded from Azure App Configuration. The values are either Strings or JSON objects, - based on there content type. + based on their content type. :rtype: ValuesView[Union[str, Mapping[str, Any]]] """ with self._update_lock: - return (self._dict).values() + return self._dict.values() @overload def get(self, key: str, default: None = None) -> Union[str, JSON, None]: ... @@ -440,7 +445,9 @@ def _update_correlation_context_header( def _deduplicate_settings(self, configuration_settings: List[ConfigurationSetting]) -> List[ConfigurationSetting]: """ - Deduplicates configuration settings by key. + Deduplicates configuration settings by key, ignoring label. The provider exposes a flat + key->value mapping, so when multiple selectors return the same key the last one wins, + regardless of label. :param List[ConfigurationSetting] configuration_settings: The list of configuration settings to deduplicate :return: A list of unique configuration settings diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py index 651a55577222..ec31ccd368eb 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager.py @@ -106,10 +106,10 @@ def _check_configuration_setting( """ Checks if the configuration setting have been updated since the last refresh. - :param str key: key to check for chances + :param str key: key to check for changes :param str label: label to check for changes - :param str etag: etag to check for changes - :param Mapping[str, str] headers: headers to use for the request + :param Optional[str] etag: etag to check for changes + :param Dict[str, str] headers: headers to use for the request :return: A tuple with the first item being true/false if a change is detected. The second item is the updated value if a change was detected. :rtype: Tuple[bool, Union[ConfigurationSetting, None]] @@ -132,7 +132,7 @@ def _check_configuration_setting( self.LOGGER.debug("Refresh all triggered by key: %s label %s.", key, label) return True, None else: - raise e + raise return False, None @distributed_trace @@ -290,10 +290,10 @@ def get_updated_watched_settings( Checks if any of the watch keys have changed, and updates them if they have. :param Mapping[Tuple[str, str], Optional[str]] watched_settings: The configuration settings to check for changes - :param Mapping[str, str] headers: The headers to use for the request + :param Dict[str, str] headers: The headers to use for the request - :return: Updated value of the configuration watched settings. - :rtype: Union[Dict[Tuple[str, str], str], None] + :return: Updated value of the configuration watched settings. Empty if no change was detected. + :rtype: Mapping[Tuple[str, str], Optional[str]] """ updated_watched_settings = dict(watched_settings) trigger_refresh = False @@ -319,8 +319,8 @@ def get_configuration_setting(self, key: str, label: str, **kwargs) -> Optional[ :param str key: The key of the configuration setting :param str label: The label of the configuration setting - :return: The configuration setting - :rtype: ConfigurationSetting + :return: The configuration setting, or None if not found + :rtype: Optional[ConfigurationSetting] """ return self._client.get_configuration_setting(key=key, label=label, **kwargs) @@ -343,7 +343,7 @@ def _validate_snapshot(self, snapshot_name: str) -> bool: if e.status_code == 404: self.LOGGER.warning("Snapshot '%s' not found when resolving snapshot.", snapshot_name) return False - raise e + raise if snapshot.composition_type != SnapshotComposition.KEY: raise ValueError(f"Composition type for '{snapshot_name}' must be 'key'.") return True @@ -403,12 +403,12 @@ def __init__( endpoint: str, credential: Optional["TokenCredential"], user_agent: str, - retry_total, - retry_backoff_max, - replica_discovery_enabled, - min_backoff_sec, - max_backoff_sec, - load_balancing_enabled, + retry_total: int, + retry_backoff_max: int, + replica_discovery_enabled: bool, + min_backoff_sec: int, + max_backoff_sec: int, + load_balancing_enabled: bool, **kwargs, ): super(ConfigurationClientManager, self).__init__( @@ -445,6 +445,7 @@ def get_next_active_client(self) -> Optional[_ConfigurationClientWrapper]: method returns None. :return: The next client to be used for the request. + :rtype: Optional[_ConfigurationClientWrapper] """ if not self._active_clients: self._last_active_client_name = "" @@ -483,10 +484,10 @@ def refresh_clients(self): if self._next_update_time and self._next_update_time > time.time(): return - failover_endpoints = find_auto_failover_endpoints(self._original_endpoint, self._replica_discovery_enabled) - - if failover_endpoints is None: - # SRV record not found, so we should refresh after a longer interval + try: + failover_endpoints = find_auto_failover_endpoints(self._original_endpoint, self._replica_discovery_enabled) + except TimeoutError: + # SRV record resolution timed out, so we should refresh after a longer interval self._next_update_time = time.time() + FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL return @@ -530,6 +531,12 @@ def refresh_clients(self): ) ) self._next_update_time = time.time() + MINIMAL_CLIENT_REFRESH_INTERVAL + # Close any replica clients that are no longer part of the failover. + retained_endpoints = {self._original_client.endpoint} + retained_endpoints.update(client.endpoint for client in discovered_clients) + for client in self._replica_clients: + if client.endpoint not in retained_endpoints: + client.close() if not self._load_balancing_enabled: random.shuffle(discovered_clients) self._replica_clients = [self._original_client] + discovered_clients diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py index cb085760012d..46218378629b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py @@ -5,13 +5,11 @@ # ------------------------------------------------------------------------- import random from dataclasses import dataclass -from typing import Optional, Mapping, Any +from typing import Optional FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL = 3600 # 1 hour in seconds MINIMAL_CLIENT_REFRESH_INTERVAL = 30 # 30 seconds -JSON = Mapping[str, Any] - @dataclass class _ConfigurationClientWrapperBase: @@ -23,12 +21,12 @@ def __init__( self, endpoint: str, user_agent: str, - retry_total, - retry_backoff_max, - replica_discovery_enabled, - min_backoff_sec, - max_backoff_sec, - _load_balancing_enabled: bool, + retry_total: int, + retry_backoff_max: int, + replica_discovery_enabled: bool, + min_backoff_sec: int, + max_backoff_sec: int, + load_balancing_enabled: bool, **kwargs, ): self._last_active_client_name = "" @@ -41,9 +39,10 @@ def __init__( self._args = dict(kwargs) self._min_backoff_sec = min_backoff_sec self._max_backoff_sec = max_backoff_sec - self._load_balancing_enabled = _load_balancing_enabled + self._load_balancing_enabled = load_balancing_enabled def _calculate_backoff(self, attempts: int) -> float: + max_attempts = 63 ms_per_second = 1000 # 1 Second in milliseconds @@ -55,8 +54,7 @@ def _calculate_backoff(self, attempts: int) -> float: calculated_milliseconds = max(1, min_backoff_milliseconds) * (1 << min(attempts, max_attempts)) - if calculated_milliseconds > max_backoff_milliseconds or calculated_milliseconds <= 0: - calculated_milliseconds = max_backoff_milliseconds + calculated_milliseconds = min(calculated_milliseconds, max_backoff_milliseconds) return min_backoff_milliseconds + ( random.uniform(0.0, 1.0) * (calculated_milliseconds - min_backoff_milliseconds) # nosec diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py index 3e68591bb46c..da34535a7329 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -19,11 +19,11 @@ # Environment Variable Constants # ------------------------------------------------------------------------ REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE = "AZURE_APP_CONFIGURATION_TRACING_DISABLED" -AzureFunctionEnvironmentVariable = "FUNCTIONS_EXTENSION_VERSION" -AzureWebAppEnvironmentVariable = "WEBSITE_SITE_NAME" -ContainerAppEnvironmentVariable = "CONTAINER_APP_NAME" -KubernetesEnvironmentVariable = "KUBERNETES_PORT" -ServiceFabricEnvironmentVariable = "Fabric_NodeName" # cspell:disable-line +AZURE_FUNCTION_ENVIRONMENT_VARIABLE = "FUNCTIONS_EXTENSION_VERSION" +AZURE_WEB_APP_ENVIRONMENT_VARIABLE = "WEBSITE_SITE_NAME" +CONTAINER_APP_ENVIRONMENT_VARIABLE = "CONTAINER_APP_NAME" +KUBERNETES_ENVIRONMENT_VARIABLE = "KUBERNETES_PORT" +SERVICE_FABRIC_ENVIRONMENT_VARIABLE = "Fabric_NodeName" # cspell:disable-line # ------------------------------------------------------------------------ # Telemetry and Tracing Constants @@ -38,9 +38,9 @@ APP_CONFIG_AI_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/" APP_CONFIG_AICC_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/chat-completion" -# ============================================================================= +# ------------------------------------------------------------------------ # Startup Retry Constants -# ============================================================================= +# ------------------------------------------------------------------------ # Timeout DEFAULT_STARTUP_TIMEOUT = 100 # seconds diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_discovery.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_discovery.py index 6dbfe722a964..5f960a1b4e08 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_discovery.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_discovery.py @@ -40,8 +40,8 @@ def find_auto_failover_endpoints(endpoint: str, replica_discovery_enabled: bool) replicas = _find_replicas(origin.target) - if not replicas: - return None # Timeout + if replicas is None: + raise TimeoutError("Timed out while resolving auto-failover replica endpoints.") srv_records = [origin] + replicas endpoints = [] diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_json.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_json.py index 3cbabde25f61..a38a79ed1cf5 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_json.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_json.py @@ -43,7 +43,7 @@ def _find_string_end(text: str, index: int) -> int: def remove_json_comments(text: str) -> str: """ - Removes comments from a JSON file. Supports //, and /* ... */ comments. + Removes comments from a JSON string. Supports //, and /* ... */ comments. Returns as string. :param text: The input JSON string with comments. diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py index d147236c1bf4..2b6b53da8494 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider.py @@ -3,14 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- -from typing import Mapping, Any, Dict +from typing import Any, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets import SecretClient, KeyVaultSecretIdentifier from azure.core.exceptions import ServiceRequestError from ._secret_provider_base import _SecretProviderBase -JSON = Mapping[str, Any] - class SecretProvider(_SecretProviderBase): @@ -72,7 +70,7 @@ def __get_secret_value(self, key: str, secret_identifier: KeyVaultSecretIdentifi def close(self) -> None: """ - Closes the connection to Azure App Configuration. + Closes the connection to Azure Key Vault. """ for client in self._secret_clients.values(): client.close() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py index 7b1abb9d97dc..a817621cde56 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_key_vault/_secret_provider_base.py @@ -4,9 +4,7 @@ # license information. # ------------------------------------------------------------------------- from typing import ( - Mapping, Any, - TypeVar, Optional, Dict, Tuple, @@ -15,9 +13,6 @@ from azure.keyvault.secrets import KeyVaultSecretIdentifier from .._azureappconfigurationproviderbase import _RefreshTimer -JSON = Mapping[str, Any] -_T = TypeVar("_T") - class _SecretProviderBase: @@ -31,7 +26,7 @@ def __init__(self, **kwargs: Any) -> None: ) if kwargs.get("secret_refresh_interval", 60) < 1: - raise ValueError("Secret refresh interval must be greater than 1 second.") + raise ValueError("Secret refresh interval must be at least 1 second.") self.secret_refresh_timer: Optional[_RefreshTimer] = ( _RefreshTimer(refresh_interval=kwargs.pop("secret_refresh_interval", 60)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_load.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_load.py index 6660ba2a6786..f763c626a512 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_load.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_load.py @@ -5,6 +5,7 @@ # ------------------------------------------------------------------------- import datetime from typing import ( + Any, Callable, List, Mapping, @@ -24,10 +25,11 @@ ) from ._azureappconfigurationprovider import ( AzureAppConfigurationProvider, - JSON, _buildprovider, ) +JSON = Mapping[str, Any] + @overload def load( # pylint: disable=docstring-keyword-should-match-keyword-only @@ -132,7 +134,7 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only :keyword str connection_string: Connection string for App Configuration resource. :keyword Optional[List[~azure.appconfiguration.provider.SettingSelector]] selects: List of setting selectors to filter configuration settings - :keyword trim_prefixes: Optional[List[str]] trim_prefixes: List of prefixes to trim from configuration keys + :keyword Optional[List[str]] trim_prefixes: List of prefixes to trim from configuration keys :keyword ~azure.core.credentials.TokenCredential keyvault_credential: A credential for authenticating with the key vault. This is optional if keyvault_client_configs is provided. :keyword Mapping[str, Mapping] keyvault_client_configs: A Mapping of SecretClient endpoints to client @@ -145,9 +147,6 @@ def load( # pylint: disable=docstring-keyword-should-match-keyword-only :keyword List[Tuple[str, str]] refresh_on: One or more settings whose modification will trigger a full refresh after a fixed interval. This should be a list of Key-Label pairs for specific settings (filters and wildcards are not supported). - :keyword refresh_on: One or more settings whose modification will trigger a full refresh after a fixed interval. - This should be a list of Key-Label pairs for specific settings (filters and wildcards are not supported). - :paramtype refresh_on: List[Tuple[str, str]] :keyword int refresh_interval: The minimum time in seconds between when a call to `refresh` will actually trigger a service call to update the settings. Default value is 30 seconds. :keyword refresh_enabled: Optional flag to enable or disable refreshing of configuration settings. Defaults to diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py index c43d3cdc91c0..9419cb2685ae 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_models.py @@ -35,7 +35,7 @@ def __init__( :keyword client_configs: A Mapping of SecretClient endpoints to client configurations from azure-keyvault-secrets. This is optional if credential is provided. If a credential isn't provided a credential will need to be in each set for each. - :paramtype client_configs: Mapping[Url, Mapping] + :paramtype client_configs: Mapping[str, Mapping[str, Any]] :keyword secret_resolver: A function that takes a URI and returns a value. :paramtype secret_resolver: Callable[[str], str] """ @@ -52,20 +52,20 @@ class SettingSelector: :keyword key_filter: A filter to select configuration settings and feature flags based on their keys. Cannot be used with snapshot_name. - :type key_filter: str + :paramtype key_filter: Optional[str] :keyword label_filter: A filter to select configuration settings and feature flags based on their labels. Default value is \0 i.e. (No Label) as seen in the portal. Cannot be used with snapshot_name. - :type label_filter: Optional[str] + :paramtype label_filter: Optional[str] :keyword tag_filters: A filter to select configuration settings and feature flags based on their tags. This is a list of strings that will be used to match tags on the configuration settings. Reserved characters (\\*, \\, ,) must be escaped with backslash if they are part of the value. Tag filters must follow the format "tagName=tagValue", for empty values use "tagName=" and for null values use "tagName=\\0". Cannot be used with snapshot_name. - :type tag_filters: Optional[List[str]] + :paramtype tag_filters: Optional[List[str]] :keyword snapshot_name: The name of the snapshot to load configuration settings from. When specified, all configuration settings from the snapshot will be loaded. Cannot be used with key_filter, label_filter, or tag_filters. - :type snapshot_name: Optional[str] + :paramtype snapshot_name: Optional[str] """ def __init__( diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_refresh_timer.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_refresh_timer.py index 9fa84c06867e..c11842a4a3d0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_refresh_timer.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_refresh_timer.py @@ -48,8 +48,7 @@ def _calculate_backoff(self) -> float: calculated_milliseconds = max(1, min_backoff_milliseconds) * (1 << min(self._attempts, max_attempts)) - if calculated_milliseconds > max_backoff_milliseconds or calculated_milliseconds <= 0: - calculated_milliseconds = max_backoff_milliseconds + calculated_milliseconds = min(calculated_milliseconds, max_backoff_milliseconds) return min_backoff_milliseconds + ( random.uniform(0.0, 1.0) * (calculated_milliseconds - min_backoff_milliseconds) # nosec diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py index bc308d0bf1ac..4c1b0a46c6d0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -8,13 +8,14 @@ from importlib.metadata import version, PackageNotFoundError from ._constants import ( REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, - ServiceFabricEnvironmentVariable, - AzureFunctionEnvironmentVariable, - AzureWebAppEnvironmentVariable, - ContainerAppEnvironmentVariable, - KubernetesEnvironmentVariable, + SERVICE_FABRIC_ENVIRONMENT_VARIABLE, + AZURE_FUNCTION_ENVIRONMENT_VARIABLE, + AZURE_WEB_APP_ENVIRONMENT_VARIABLE, + CONTAINER_APP_ENVIRONMENT_VARIABLE, + KUBERNETES_ENVIRONMENT_VARIABLE, APP_CONFIG_AI_MIME_PROFILE, APP_CONFIG_AICC_MIME_PROFILE, + SNAPSHOT_REFERENCE_TAG, ) # Feature flag filter names @@ -29,17 +30,11 @@ FEATURE_FLAG_USES_TELEMETRY_TAG = "Telemetry" FEATURE_FLAG_USES_SEED_TAG = "Seed" -FEATURE_FLAG_MAX_VARIANTS_KEY = "MaxVariants" -FEATURE_FLAG_FEATURES_KEY = "FFFeatures" - -CHAT_COMPLETION_PROFILE = "chat-completion" - # Feature flag constants for telemetry LOAD_BALANCING_FEATURE = "LB" AI_CONFIGURATION_FEATURE = "AI" AI_CHAT_COMPLETION_FEATURE = "AICC" -SNAPSHOT_REFERENCE_TAG = "SnapshotRef" # Correlation context constants FEATUREMANAGEMENT_PACKAGE = "featuremanagement" @@ -240,15 +235,13 @@ def update_feature_filter_telemetry(self, feature_flag) -> None: :param feature_flag: The feature flag to analyze for filter usage. :type feature_flag: FeatureFlagConfigurationSetting """ - # Constants are already imported at module level - if feature_flag.filters: - for filter in feature_flag.filters: - if filter.get("name") in PERCENTAGE_FILTER_NAMES: + for feature_filter in feature_flag.filters: + if feature_filter.get("name") in PERCENTAGE_FILTER_NAMES: self.feature_filter_usage[PERCENTAGE_FILTER_KEY] = True - elif filter.get("name") in TIME_WINDOW_FILTER_NAMES: + elif feature_filter.get("name") in TIME_WINDOW_FILTER_NAMES: self.feature_filter_usage[TIME_WINDOW_FILTER_KEY] = True - elif filter.get("name") in TARGETING_FILTER_NAMES: + elif feature_filter.get("name") in TARGETING_FILTER_NAMES: self.feature_filter_usage[TARGETING_FILTER_KEY] = True else: self.feature_filter_usage[CUSTOM_FILTER_KEY] = True @@ -322,15 +315,15 @@ def get_host_type() -> str: :return: The detected host type. :rtype: str """ - if os.environ.get(AzureFunctionEnvironmentVariable): + if os.environ.get(AZURE_FUNCTION_ENVIRONMENT_VARIABLE): return HostType.AZURE_FUNCTION - if os.environ.get(AzureWebAppEnvironmentVariable): + if os.environ.get(AZURE_WEB_APP_ENVIRONMENT_VARIABLE): return HostType.AZURE_WEB_APP - if os.environ.get(ContainerAppEnvironmentVariable): + if os.environ.get(CONTAINER_APP_ENVIRONMENT_VARIABLE): return HostType.CONTAINER_APP - if os.environ.get(KubernetesEnvironmentVariable): + if os.environ.get(KUBERNETES_ENVIRONMENT_VARIABLE): return HostType.KUBERNETES - if os.environ.get(ServiceFabricEnvironmentVariable): + if os.environ.get(SERVICE_FABRIC_ENVIRONMENT_VARIABLE): return HostType.SERVICE_FABRIC return HostType.UNIDENTIFIED diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_snapshot_reference_parser.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_snapshot_reference_parser.py index 08213687f188..f1dda27a84b0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_snapshot_reference_parser.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_snapshot_reference_parser.py @@ -20,7 +20,8 @@ def parse(setting: Optional[ConfigurationSetting]) -> str: """ Parse a snapshot reference from a configuration setting containing snapshot reference JSON. - :param Optional[ConfigurationSetting] setting: The configuration setting containing the snapshot reference JSON + :param setting: The configuration setting containing the snapshot reference JSON + :type setting: Optional[ConfigurationSetting] :return: The snapshot name extracted from the reference :rtype: str :raises ValueError: When the setting is None @@ -63,13 +64,15 @@ def parse(setting: Optional[ConfigurationSetting]) -> str: f"property must be a string value, but found {type(snapshot_name).__name__}." ) - if not snapshot_name.strip(): + snapshot_name = snapshot_name.strip() + + if not snapshot_name: raise ValueError( f"Invalid snapshot reference format for key '{setting.key}' " f"(label: '{setting.label}'). Snapshot name cannot be empty or whitespace." ) - return snapshot_name.strip() + return snapshot_name except json.JSONDecodeError as json_ex: raise ValueError( diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py index be1b4d6a850e..33f209ae605b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_utils.py @@ -9,7 +9,6 @@ from typing import ( Any, Dict, - Mapping, Optional, Tuple, ) @@ -21,9 +20,6 @@ STARTUP_BACKOFF_INTERVALS, ) - -JSON = Mapping[str, Any] - min_uptime = 5 @@ -47,7 +43,7 @@ def get_startup_backoff(elapsed_seconds: float, attempts: int) -> Tuple[float, b :param elapsed_seconds: The time elapsed since startup began, in seconds. :type elapsed_seconds: float - :param attempts: The number of retry attempts made (1-based). + :param attempts: The number of retry attempts made (0-based). :type attempts: int :return: A tuple where the first element is the backoff duration in seconds, and the second element indicates if the fixed backoff window has been exceeded. @@ -117,7 +113,7 @@ def process_load_parameters(*args, **kwargs: Any) -> Dict[str, Any]: # Get startup timeout startup_timeout = kwargs.pop("startup_timeout", DEFAULT_STARTUP_TIMEOUT) if startup_timeout < 0: - raise ValueError("Startup timeout must be greater than or equal to 0 seconds.") + raise ValueError("Startup timeout must be at least 1 second.") return { "endpoint": endpoint, @@ -194,18 +190,16 @@ def _calculate_backoff_duration(attempts: int) -> float: """ attempts += 1 if attempts < 1: - raise ValueError("Number of attempts must be at least 1.") + raise ValueError("Number of attempts must be at least 0.") if attempts == 1: return MIN_STARTUP_EXPONENTIAL_BACKOFF_DURATION # Calculate exponential backoff: min * 2^(attempts-1) - # Cap the shift amount to prevent overflow safe_shift = min(attempts - 1, 63) calculated = MIN_STARTUP_EXPONENTIAL_BACKOFF_DURATION * (1 << safe_shift) # Cap at max duration - if calculated > MAX_STARTUP_BACKOFF_DURATION or calculated <= 0: # Check for overflow - calculated = MAX_STARTUP_BACKOFF_DURATION + calculated = min(calculated, MAX_STARTUP_BACKOFF_DURATION) return _jitter(calculated, JITTER_RATIO) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_client_manager.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_client_manager.py index abcb6233e9a1..758caf4177fd 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_client_manager.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_client_manager.py @@ -108,10 +108,10 @@ async def _check_configuration_setting( """ Checks if the configuration setting have been updated since the last refresh. - :param str key: key to check for chances + :param str key: key to check for changes :param str label: label to check for changes - :param str etag: etag to check for changes - :param Mapping[str, str] headers: headers to use for the request + :param Optional[str] etag: etag to check for changes + :param Dict[str, str] headers: headers to use for the request :return: A tuple with the first item being true/false if a change is detected. The second item is the updated value if a change was detected. :rtype: Tuple[bool, Union[ConfigurationSetting, None]] @@ -134,7 +134,7 @@ async def _check_configuration_setting( self.LOGGER.debug("Refresh all triggered by key: %s label %s.", key, label) return True, None else: - raise e + raise return False, None @distributed_trace @@ -292,10 +292,10 @@ async def get_updated_watched_settings( Checks if any of the watch keys have changed, and updates them if they have. :param Mapping[Tuple[str, str], Optional[str]] watched_settings: The configuration settings to check for changes - :param Mapping[str, str] headers: The headers to use for the request + :param Dict[str, str] headers: The headers to use for the request - :return: Updated value of the configuration watched settings. - :rtype: Union[Dict[Tuple[str, str], str], None] + :return: Updated value of the configuration watched settings. Empty if no change was detected. + :rtype: Mapping[Tuple[str, str], Optional[str]] """ updated_watched_settings = dict(watched_settings) trigger_refresh = False @@ -321,8 +321,8 @@ async def get_configuration_setting(self, key: str, label: str, **kwargs) -> Opt :param str key: The key of the configuration setting :param str label: The label of the configuration setting - :return: The configuration setting - :rtype: ConfigurationSetting + :return: The configuration setting, or None if not found + :rtype: Optional[ConfigurationSetting] """ return await self._client.get_configuration_setting(key=key, label=label, **kwargs) @@ -345,7 +345,7 @@ async def _validate_snapshot(self, snapshot_name: str) -> bool: if e.status_code == 404: self.LOGGER.warning("Snapshot '%s' not found when resolving snapshot.", snapshot_name) return False - raise e + raise if snapshot.composition_type != SnapshotComposition.KEY: raise ValueError(f"Composition type for '{snapshot_name}' must be 'key'.") return True @@ -405,12 +405,12 @@ def __init__( endpoint: str, credential: Optional["AsyncTokenCredential"], user_agent: str, - retry_total, - retry_backoff_max, - replica_discovery_enabled, - min_backoff_sec, - max_backoff_sec, - load_balancing_enabled, + retry_total: int, + retry_backoff_max: int, + replica_discovery_enabled: bool, + min_backoff_sec: int, + max_backoff_sec: int, + load_balancing_enabled: bool, **kwargs, ): super(AsyncConfigurationClientManager, self).__init__( @@ -447,6 +447,7 @@ def get_next_active_client(self) -> Optional[_AsyncConfigurationClientWrapper]: method returns None. :return: The next client to be used for the request. + :rtype: Optional[_AsyncConfigurationClientWrapper] """ if not self._active_clients: self._last_active_client_name = "" @@ -485,12 +486,12 @@ async def refresh_clients(self): if self._next_update_time and self._next_update_time > time.time(): return - failover_endpoints = await find_auto_failover_endpoints( - self._original_endpoint, self._replica_discovery_enabled - ) - - if failover_endpoints is None: - # SRV record not found, so we should refresh after a longer interval + try: + failover_endpoints = await find_auto_failover_endpoints( + self._original_endpoint, self._replica_discovery_enabled + ) + except TimeoutError: + # SRV record resolution timed out, so we should refresh after a longer interval self._next_update_time = time.time() + FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL return @@ -534,6 +535,12 @@ async def refresh_clients(self): ) ) self._next_update_time = time.time() + MINIMAL_CLIENT_REFRESH_INTERVAL + # Close any replica clients that are no longer part of the failover. + retained_endpoints = {self._original_client.endpoint} + retained_endpoints.update(client.endpoint for client in discovered_clients) + for client in self._replica_clients: + if client.endpoint not in retained_endpoints: + await client.close() if not self._load_balancing_enabled: random.shuffle(discovered_clients) self._replica_clients = [self._original_client] + discovered_clients diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_discovery.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_discovery.py index 1713c0d0d833..ecc26d0a7e0e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_discovery.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_discovery.py @@ -25,8 +25,8 @@ async def find_auto_failover_endpoints(endpoint: str, replica_discovery_enabled: replicas = await _find_replicas(origin.target) - if not replicas: - return None # Timeout + if replicas is None: + raise TimeoutError("Timed out while resolving auto-failover replica endpoints.") srv_records = [origin] + replicas endpoints = [] diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_load.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_load.py index 81a8854340aa..bf2f36732906 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_load.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_async_load.py @@ -135,7 +135,7 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only :keyword str connection_string: Connection string for App Configuration resource. :keyword Optional[List[~azure.appconfiguration.provider.SettingSelector]] selects: List of setting selectors to filter configuration settings - :keyword trim_prefixes: Optional[List[str]] trim_prefixes: List of prefixes to trim from configuration keys + :keyword Optional[List[str]] trim_prefixes: List of prefixes to trim from configuration keys :keyword ~azure.core.credentials_async.AsyncTokenCredential keyvault_credential: A credential for authenticating with the key vault. This is optional if keyvault_client_configs is provided. :keyword Mapping[str, Mapping] keyvault_client_configs: A Mapping of SecretClient endpoints to client @@ -148,9 +148,6 @@ async def load( # pylint: disable=docstring-keyword-should-match-keyword-only :keyword List[Tuple[str, str]] refresh_on: One or more settings whose modification will trigger a full refresh after a fixed interval. This should be a list of Key-Label pairs for specific settings (filters and wildcards are not supported). - :keyword refresh_on: One or more settings whose modification will trigger a full refresh after a fixed interval. - This should be a list of Key-Label pairs for specific settings (filters and wildcards are not supported). - :paramtype refresh_on: List[Tuple[str, str]] :keyword int refresh_interval: The minimum time in seconds between when a call to `refresh` will actually trigger a service call to update the settings. Default value is 30 seconds. :keyword refresh_enabled: Optional flag to enable or disable refreshing of configuration settings. Defaults to diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 458bd10bcce8..99d03a587468 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -41,7 +41,6 @@ from .._user_agent import USER_AGENT from .._utils import get_startup_backoff -JSON = Mapping[str, Any] logger = logging.getLogger(__name__) @@ -110,6 +109,16 @@ def __init__(self, **kwargs: Any) -> None: async def _attempt_refresh( self, client: ConfigurationClient, replica_count: int, is_failover_request: bool, **kwargs ): + """ + Attempts to refresh configuration settings and feature flags using a single client. + + :param client: The configuration client to attempt the refresh against. + :type client: ~azure.appconfiguration.provider.aio.ConfigurationClient + :param replica_count: The number of replica clients available, used for correlation telemetry. + :type replica_count: int + :param is_failover_request: Whether this attempt is a failover from a previously failed client. + :type is_failover_request: bool + """ settings_refreshed = False headers = self._update_correlation_context_header( kwargs.pop("headers", {}), diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py index 70c61840d087..5f32d73b778e 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_key_vault/_async_secret_provider.py @@ -4,15 +4,13 @@ # license information. # ------------------------------------------------------------------------- import inspect -from typing import Mapping, Any, Dict +from typing import Any, Dict from azure.appconfiguration import SecretReferenceConfigurationSetting # type:ignore # pylint:disable=no-name-in-module from azure.keyvault.secrets import KeyVaultSecretIdentifier from azure.keyvault.secrets.aio import SecretClient from azure.core.exceptions import ServiceRequestError from ..._key_vault._secret_provider_base import _SecretProviderBase -JSON = Mapping[str, Any] - class SecretProvider(_SecretProviderBase): @@ -78,7 +76,7 @@ async def __get_secret_value(self, key: str, secret_identifier: KeyVaultSecretId async def close(self) -> None: """ - Closes the connection to Azure App Configuration. + Closes the connection to Azure Key Vault. """ for client in self._secret_clients.values(): await client.close() diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager.py index e8c2c66dc2d9..956e986637f0 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager.py @@ -27,6 +27,9 @@ def __init__(self, endpoint, connection_string, credential, retry_total, retry_b self.retry_total = retry_total self.retry_backoff = retry_backoff + async def close(self): + pass + @pytest.mark.usefixtures("caplog") class TestAsyncConfigurationClientManager: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager_load_balance.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager_load_balance.py index f252798367b1..7d5ac234453a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager_load_balance.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_configuration_async_client_manager_load_balance.py @@ -24,6 +24,9 @@ def __init__(self, endpoint, connection_string, credential, retry_total, retry_b self.retry_total = retry_total self.retry_backoff = retry_backoff + async def close(self): + pass + @pytest.mark.usefixtures("caplog") class TestConfigurationAsyncClientManagerLoadBalance: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager.py index 5edaab158bf4..d2123f6f4f3a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager.py @@ -26,6 +26,9 @@ def __init__(self, endpoint, connection_string, credential, retry_total, retry_b self.retry_total = retry_total self.retry_backoff = retry_backoff + def close(self): + pass + @pytest.mark.usefixtures("caplog") class TestConfigurationClientManager(unittest.TestCase): diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager_load_balance.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager_load_balance.py index f73942364e19..3d0fca436379 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager_load_balance.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_configuration_client_manager_load_balance.py @@ -25,6 +25,9 @@ def __init__(self, endpoint, connection_string, credential, retry_total, retry_b self.retry_total = retry_total self.retry_backoff = retry_backoff + def close(self): + pass + @pytest.mark.usefixtures("caplog") class TestConfigurationClientManagerLoadBalance(unittest.TestCase): diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py index ad3cf6285d8b..97084765b698 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py @@ -24,11 +24,11 @@ ) from azure.appconfiguration.provider._constants import ( REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, - ServiceFabricEnvironmentVariable, - AzureFunctionEnvironmentVariable, - AzureWebAppEnvironmentVariable, - ContainerAppEnvironmentVariable, - KubernetesEnvironmentVariable, + SERVICE_FABRIC_ENVIRONMENT_VARIABLE, + AZURE_FUNCTION_ENVIRONMENT_VARIABLE, + AZURE_WEB_APP_ENVIRONMENT_VARIABLE, + CONTAINER_APP_ENVIRONMENT_VARIABLE, + KUBERNETES_ENVIRONMENT_VARIABLE, APP_CONFIG_AI_MIME_PROFILE, APP_CONFIG_AICC_MIME_PROFILE, SNAPSHOT_REFERENCE_TAG, @@ -115,31 +115,31 @@ def test_get_host_type_unidentified(self): def test_get_host_type_azure_function(self): """Test host type detection for Azure Functions.""" - with patch.dict(os.environ, {AzureFunctionEnvironmentVariable: "test_value"}): + with patch.dict(os.environ, {AZURE_FUNCTION_ENVIRONMENT_VARIABLE: "test_value"}): result = _RequestTracingContext.get_host_type() self.assertEqual(result, HostType.AZURE_FUNCTION) def test_get_host_type_azure_web_app(self): """Test host type detection for Azure Web App.""" - with patch.dict(os.environ, {AzureWebAppEnvironmentVariable: "test_value"}): + with patch.dict(os.environ, {AZURE_WEB_APP_ENVIRONMENT_VARIABLE: "test_value"}): result = _RequestTracingContext.get_host_type() self.assertEqual(result, HostType.AZURE_WEB_APP) def test_get_host_type_container_app(self): """Test host type detection for Container App.""" - with patch.dict(os.environ, {ContainerAppEnvironmentVariable: "test_value"}): + with patch.dict(os.environ, {CONTAINER_APP_ENVIRONMENT_VARIABLE: "test_value"}): result = _RequestTracingContext.get_host_type() self.assertEqual(result, HostType.CONTAINER_APP) def test_get_host_type_kubernetes(self): """Test host type detection for Kubernetes.""" - with patch.dict(os.environ, {KubernetesEnvironmentVariable: "test_value"}): + with patch.dict(os.environ, {KUBERNETES_ENVIRONMENT_VARIABLE: "test_value"}): result = _RequestTracingContext.get_host_type() self.assertEqual(result, HostType.KUBERNETES) def test_get_host_type_service_fabric(self): """Test host type detection for Service Fabric.""" - with patch.dict(os.environ, {ServiceFabricEnvironmentVariable: "test_value"}): + with patch.dict(os.environ, {SERVICE_FABRIC_ENVIRONMENT_VARIABLE: "test_value"}): result = _RequestTracingContext.get_host_type() self.assertEqual(result, HostType.SERVICE_FABRIC) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_utils.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_utils.py index 9b2f23668dfd..4b1bbb90f8d5 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_utils.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_utils.py @@ -187,7 +187,7 @@ def test_invalid_attempts_raises_error(self): # Input -1 -> internal 0, which is < 1 with self.assertRaises(ValueError) as context: _calculate_backoff_duration(-1) - self.assertIn("Number of attempts must be at least 1", str(context.exception)) + self.assertIn("Number of attempts must be at least 0", str(context.exception)) with self.assertRaises(ValueError): _calculate_backoff_duration(-2)