diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java index 67f9e17256b0..14882dd17047 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java @@ -10,20 +10,49 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.stores.AppConfigurationSecretClientManager; import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory; +/** + * Factory for creating and managing AppConfigurationSecretClientManager instances. This class caches clients per Key + * Vault host. + */ class AppConfigurationKeyVaultClientFactory { + /** + * Cache of secret client managers by Key Vault host. private final Map keyVaultClients; + /** + * Optional customizer for Key Vault secret clients. + */ private final SecretClientCustomizer keyVaultClientProvider; + /** + * Optional provider for custom secret resolution. + */ private final KeyVaultSecretProvider keyVaultSecretProvider; + /** + * Factory for creating secret client builders. + */ private final SecretClientBuilderFactory secretClientFactory; - + + /** + * Flag indicating whether credentials are configured. + */ private final boolean credentialsConfigured; + /** + * Flag indicating whether the factory being used for telemetry. + */ private final boolean isConfigured; + /** + * Creates a new AppConfigurationKeyVaultClientFactory. + * + * @param keyVaultClientProvider optional customizer for Key Vault secret clients + * @param keyVaultSecretProvider optional provider for custom secret resolution + * @param secretClientFactory factory for creating secret client builders + * @param credentialsConfigured whether credentials are configured + */ AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider, KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory, boolean credentialsConfigured) { @@ -35,6 +64,12 @@ class AppConfigurationKeyVaultClientFactory { isConfigured = keyVaultClientProvider != null || credentialsConfigured; } + /** + * Gets or creates a secret client manager for the specified Key Vault host. + * + * @param host the Key Vault host endpoint + * @return the secret client manager for the host + */ AppConfigurationSecretClientManager getClient(String host) { // Check if we already have a client for this key vault, if not we will make // one @@ -46,7 +81,12 @@ AppConfigurationSecretClientManager getClient(String host) { return keyVaultClients.get(host); } - public boolean isConfigured() { + /** + * Returns if Key Vault is configured to be used. + * + * @return true if either a client provider is configured or credentials are configured + */ + boolean isConfigured() { return isConfigured; } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java index 9515c30258a9..c2ce5df5e3f0 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java @@ -11,22 +11,31 @@ import org.springframework.core.env.EnumerablePropertySource; import com.azure.core.util.Context; -import com.azure.data.appconfiguration.ConfigurationClient; /** - * Azure App Configuration PropertySource unique per Store Label(Profile) combo. - * + * Abstract base class for Azure App Configuration PropertySource implementations. + * *

- * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be - * created. + * Each PropertySource is unique per Store-Label(Profile) combination. For example, if connecting to 2 stores with 2 + * labels each, 4 AppConfigurationPropertySources need to be created. *

*/ -abstract class AppConfigurationPropertySource extends EnumerablePropertySource { +abstract class AppConfigurationPropertySource extends EnumerablePropertySource { + /** protected final Map properties = new LinkedHashMap<>(); + /** + * Client for communicating with Azure App Configuration service. + */ protected final AppConfigurationReplicaClient replicaClient; + /** + * Creates a new AppConfigurationPropertySource. + * + * @param name the name of this property source, should be unique to identify the store-label combination + * @param replicaClient the client for communicating with Azure App Configuration + */ AppConfigurationPropertySource(String name, AppConfigurationReplicaClient replicaClient) { // The context alone does not uniquely define a PropertySource, append storeName // and label to uniquely define a PropertySource @@ -34,17 +43,32 @@ abstract class AppConfigurationPropertySource extends EnumerablePropertySource keySet = properties.keySet(); return keySet.toArray(new String[keySet.size()]); } + /** + * Returns the value of the specified property. + * + */ @Override public Object getProperty(String name) { return properties.get(name); } + /** + * Creates a comma-separated string from the given label filters. + * + * @param labelFilters array of label filters, may be null + * @return comma-separated string of labels, or empty string if null/empty + */ protected static String getLabelName(String[] labelFilters) { if (labelFilters == null) { return ""; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java index c45799b10629..cb5d5735d39b 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java @@ -20,32 +20,56 @@ import reactor.core.publisher.Mono; /** - * Enables checking of Configuration updates. + * Component responsible for checking Azure App Configuration for updates and triggering refresh events. */ @Component public class AppConfigurationPullRefresh implements AppConfigurationRefresh { private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPullRefresh.class); + /** + * Flag to prevent concurrent refresh operations. + */ private final AtomicBoolean running = new AtomicBoolean(false); + /** + * Publisher for Spring refresh events. + */ private ApplicationEventPublisher publisher; - private final Long defaultMinBackoff = (long) 30; + /** + * Default minimum backoff duration in seconds when refresh operations fail. + */ + private static final Long DEFAULT_MIN_BACKOFF_SECONDS = 30L; + + /** + * Factory for creating App Configuration replica clients. + */ private final AppConfigurationReplicaClientFactory clientFactory; + /** + * Time interval between configuration refresh checks. + */ private final Duration refreshInterval; - + + /** + * Component for replica lookup and failover functionality. + */ private final ReplicaLookUp replicaLookUp; - + + /** + * Utility component for refresh operations. + */ private final AppConfigurationRefreshUtil refreshUtils; /** - * Component used for checking for and triggering configuration refreshes. + * Creates a new AppConfigurationPullRefresh component. * - * @param clientFactory Clients stores used to connect to App Configuration. * @param defaultMinBackoff default - * @param refreshInterval time between refresh intervals + * @param clientFactory factory for creating App Configuration clients to connect to stores + * @param refreshInterval time duration between refresh interval checks + * @param replicaLookUp component for handling replica lookup and failover + * @param refreshUtils utility component for refresh operations */ public AppConfigurationPullRefresh(AppConfigurationReplicaClientFactory clientFactory, Duration refreshInterval, ReplicaLookUp replicaLookUp, AppConfigurationRefreshUtil refreshUtils) { @@ -53,20 +77,24 @@ public AppConfigurationPullRefresh(AppConfigurationReplicaClientFactory clientFa this.clientFactory = clientFactory; this.replicaLookUp = replicaLookUp; this.refreshUtils = refreshUtils; - } + /** + * Sets the Spring application event publisher for publishing refresh events. + * + * @param applicationEventPublisher the Spring event publisher to use for refresh events + */ @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } /** - * Checks configurations to see if configurations should be reloaded. If the refresh interval has passed and a - * trigger has been updated configuration are reloaded. - * - * @return Future with a boolean of if a RefreshEvent was published. If refreshConfigurations is currently being run - * elsewhere this method will return right away as false. + * Checks configurations to see if they should be reloaded. If the refresh interval has passed and a trigger has + * been updated, configurations are reloaded. + * + * @return a Mono containing a boolean indicating if a RefreshEvent was published. Returns {@code false} if + * refreshConfigurations is currently being executed elsewhere. */ public Mono refreshConfigurations() { return Mono.just(refreshStores()); @@ -74,9 +102,10 @@ public Mono refreshConfigurations() { /** * Soft expires refresh interval. Sets amount of time to next refresh to be a random value between 0 and 15 seconds, - * unless value is less than the amount of time to the next refresh check. - * @param endpoint Config Store endpoint to expire refresh interval on. - * @param syncToken syncToken to verify the latest changes are available on pull + * unless that value is less than the amount of time to the next refresh check. + * + * @param endpoint the Config Store endpoint to expire refresh interval on + * @param syncToken the syncToken to verify the latest changes are available on pull */ public void expireRefreshInterval(String endpoint, String syncToken) { LOGGER.debug("Expiring refresh interval for " + endpoint); @@ -90,22 +119,23 @@ public void expireRefreshInterval(String endpoint, String syncToken) { /** * Goes through each config store and checks if any of its keys need to be refreshed. If any store has a value that - * needs to be updated a refresh event is called after every store is checked. + * needs to be updated, a refresh event is called after every store is checked. * - * @return If a refresh event is called. + * @return true if a refresh event is published, false otherwise */ private boolean refreshStores() { if (running.compareAndSet(false, true)) { try { RefreshEventData eventData = refreshUtils.refreshStoresCheck(clientFactory, - refreshInterval, defaultMinBackoff, replicaLookUp); + refreshInterval, DEFAULT_MIN_BACKOFF_SECONDS, replicaLookUp); if (eventData.getDoRefresh()) { publisher.publishEvent(new RefreshEvent(this, eventData, eventData.getMessage())); return true; } } catch (Exception e) { + LOGGER.warn("Error occurred during configuration refresh, will retry at next interval", e); // The next refresh will happen sooner if refresh interval is expired. - StateHolder.getCurrentState().updateNextRefreshTime(refreshInterval, defaultMinBackoff); + StateHolder.getCurrentState().updateNextRefreshTime(refreshInterval, DEFAULT_MIN_BACKOFF_SECONDS); throw e; } finally { running.set(false); @@ -114,6 +144,11 @@ private boolean refreshStores() { return false; } + /** + * Gets the health status of all configured App Configuration stores. + * + * @return a map containing the health status of each store, keyed by store identifier + */ @Override public Map getAppConfigurationStoresHealth() { return clientFactory.getHealth(); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java index a5337b08e43a..bca843fdfe2c 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java @@ -3,6 +3,7 @@ package com.azure.spring.cloud.appconfiguration.config.implementation; import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; + import java.time.Duration; import java.time.Instant; import java.util.List; @@ -22,15 +23,21 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore; +/** + * Utility class for handling Azure App Configuration refresh operations. + */ public class AppConfigurationRefreshUtil { - private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPullRefresh.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationRefreshUtil.class); /** - * Goes through each config store and checks if any of its keys need to be refreshed. If any store has a value that - * needs to be updated a refresh event is called after every store is checked. + * Checks all configured stores to determine if any configurations need to be refreshed. * - * @return If a refresh event is called. + * @param clientFactory factory for accessing App Configuration clients + * @param refreshInterval the time interval between refresh checks + * @param defaultMinBackoff the minimum backoff time for failed operations + * @param replicaLookUp component for handling replica failover + * @return RefreshEventData containing information about whether a refresh should occur */ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientFactory, Duration refreshInterval, Long defaultMinBackoff, ReplicaLookUp replicaLookUp) { @@ -78,9 +85,11 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF break; } catch (HttpResponseException e) { LOGGER.warn( - "Failed attempting to connect to " + client.getEndpoint() + " during refresh check."); + "Failed to connect to App Configuration store {} during configuration refresh check. " + + "Status: {}, Message: {}", + client.getEndpoint(), e.getResponse().getStatusCode(), e.getMessage()); - clientFactory.backoffClientClient(originEndpoint, client.getEndpoint()); + clientFactory.backoffClient(originEndpoint, client.getEndpoint()); } } } else { @@ -102,9 +111,11 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF break; } catch (HttpResponseException e) { LOGGER.warn( - "Failed attempting to connect to " + client.getEndpoint() + " during refresh check."); + "Failed to connect to App Configuration store {} during feature flag refresh check. " + + "Status: {}, Message: {}", + client.getEndpoint(), e.getResponse().getStatusCode(), e.getMessage()); - clientFactory.backoffClientClient(originEndpoint, client.getEndpoint()); + clientFactory.backoffClient(originEndpoint, client.getEndpoint()); } } } else { @@ -121,11 +132,13 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF } /** - * This is for a refresh fail only. + * Performs a refresh check for a specific store client without time constraints. This method is used for refresh + * failure scenarios only. * - * @param client Client checking for refresh - * @param originEndpoint config store origin endpoint - * @return A refresh should be triggered. + * @param client the client for checking refresh status + * @param originEndpoint the original config store endpoint + * @param context the operation context + * @return true if a refresh should be triggered, false otherwise */ static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String originEndpoint, Context context) { RefreshEventData eventData = new RefreshEventData(); @@ -136,11 +149,13 @@ static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String or } /** - * This is for a refresh fail only. + * Performs a feature flag refresh check for a specific store client. This method is used for refresh failure + * scenarios only. * - * @param featureStore Feature info for the store - * @param client Client checking for refresh - * @return true if a refresh should be triggered. + * @param featureStoreEnabled whether feature store is enabled + * @param client the client for checking refresh status + * @param context the operation context + * @return true if a refresh should be triggered, false otherwise */ static boolean refreshStoreFeatureFlagCheck(Boolean featureStoreEnabled, AppConfigurationReplicaClient client, Context context) { @@ -156,14 +171,20 @@ static boolean refreshStoreFeatureFlagCheck(Boolean featureStoreEnabled, } /** - * Checks refresh trigger for etag changes. If they have changed a RefreshEventData is published. + * Checks configuration refresh triggers for etag changes with time-based validation. Only performs the refresh + * check if the refresh interval has elapsed. * - * @param state The refresh state of the endpoint being checked. - * @param refreshInterval Amount of time to wait until next check of this endpoint. - * @param eventData Info for this refresh event. + * @param client the App Configuration client to use for checking + * @param state the current refresh state of the endpoint being checked + * @param refreshInterval the time duration to wait until next check of this endpoint + * @param eventData the refresh event data to update if changes are detected + * @param replicaLookUp component for updating auto-failover endpoints + * @param context the operation context + * @throws AppConfigurationStatusException if there's an error during the refresh check */ private static void refreshWithTime(AppConfigurationReplicaClient client, State state, Duration refreshInterval, - RefreshEventData eventData, ReplicaLookUp replicaLookUp, Context context) throws AppConfigurationStatusException { + RefreshEventData eventData, ReplicaLookUp replicaLookUp, Context context) + throws AppConfigurationStatusException { if (Instant.now().isAfter(state.getNextRefreshCheck())) { replicaLookUp.updateAutoFailoverEndpoints(); refreshWithoutTime(client, state.getWatchKeys(), eventData, context); @@ -173,11 +194,14 @@ private static void refreshWithTime(AppConfigurationReplicaClient client, State } /** - * Checks refresh trigger for etag changes. If they have changed a RefreshEventData is published. + * Checks configuration refresh triggers for etag changes without time validation. This method immediately checks + * all watch keys for changes regardless of refresh intervals. * - * @param client Client checking for refresh - * @param watchKeys Watch keys for the store. - * @param eventData Refresh event info + * @param client the App Configuration client to use for checking + * @param watchKeys the list of configuration settings to watch for changes + * @param eventData the refresh event data to update if changes are detected + * @param context the operation context + * @throws AppConfigurationStatusException if there's an error during the refresh check */ private static void refreshWithoutTime(AppConfigurationReplicaClient client, List watchKeys, RefreshEventData eventData, Context context) throws AppConfigurationStatusException { @@ -195,6 +219,18 @@ private static void refreshWithoutTime(AppConfigurationReplicaClient client, Lis } } + /** + * Checks feature flag refresh triggers with time-based validation. Only performs the refresh check if the refresh + * interval has elapsed. + * + * @param client the App Configuration client to use for checking + * @param state the current feature flag state of the endpoint being checked + * @param refreshInterval the time duration to wait until next check of this endpoint + * @param eventData the refresh event data to update if changes are detected + * @param replicaLookUp component for updating auto-failover endpoints + * @param context the operation context + * @throws AppConfigurationStatusException if there's an error during the refresh check + */ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagState state, Duration refreshInterval, RefreshEventData eventData, ReplicaLookUp replicaLookUp, Context context) throws AppConfigurationStatusException { @@ -220,6 +256,16 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl } } + /** + * Checks feature flag refresh triggers without time validation. This method immediately checks all feature flag + * watch keys for changes regardless of refresh intervals. + * + * @param client the App Configuration client to use for checking + * @param watchKeys the feature flag state containing watch keys to check for changes + * @param eventData the refresh event data to update if changes are detected + * @param context the operation context + * @throws AppConfigurationStatusException if there's an error during the refresh check + */ private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagState watchKeys, RefreshEventData eventData, Context context) throws AppConfigurationStatusException { @@ -237,13 +283,23 @@ private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient } } + /** + * Checks the etag values between watched and current configuration settings to determine if a refresh is needed. + * + * @param watchSetting the configuration setting being watched for changes + * @param currentTriggerConfiguration the current configuration setting from the store + * @param endpoint the endpoint of the configuration store + * @param eventData the refresh event data to update if a change is detected + */ private static void checkETag(ConfigurationSetting watchSetting, ConfigurationSetting currentTriggerConfiguration, String endpoint, RefreshEventData eventData) { if (currentTriggerConfiguration == null) { return; } - LOGGER.debug(watchSetting.getETag(), " - ", currentTriggerConfiguration.getETag()); + LOGGER.debug("Comparing eTags - watched: {} vs current: {}", + watchSetting.getETag(), currentTriggerConfiguration.getETag()); + if (watchSetting.getETag() != null && !watchSetting.getETag().equals(currentTriggerConfiguration.getETag())) { LOGGER.trace("Some keys in store [{}] matching the key [{}] and label [{}] is updated, " + "will send refresh event.", endpoint, watchSetting.getKey(), watchSetting.getLabel()); @@ -258,7 +314,7 @@ private static void checkETag(ConfigurationSetting watchSetting, ConfigurationSe } /** - * For each refresh, multiple etags can change, but even one etag is changed, refresh is required. + * Data structure containing information about a refresh event. */ static class RefreshEventData { @@ -268,25 +324,50 @@ static class RefreshEventData { private boolean doRefresh = false; + /** + * Creates a new RefreshEventData with empty message and refresh flag set to false. + */ RefreshEventData() { this.message = ""; } + /** + * Sets the refresh message using the standard message template. + * + * @param prefix the prefix to include in the message (typically a key name) + * @return this RefreshEventData instance for method chaining + */ RefreshEventData setMessage(String prefix) { setFullMessage(String.format(MSG_TEMPLATE, prefix)); return this; } + /** + * Sets the full refresh message and marks that a refresh should occur. + * + * @param message the complete message describing the refresh event + * @return this RefreshEventData instance for method chaining + */ private RefreshEventData setFullMessage(String message) { this.message = message; this.doRefresh = true; return this; } + /** + * Gets the refresh event message. + * + * @return the message describing what triggered the refresh + */ public String getMessage() { return this.message; } + /** + * Indicates whether a refresh should be performed. + * + * @return true if a refresh is needed, false otherwise + */ public boolean getDoRefresh() { return doRefresh; } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java index 5c192dc04dbc..ed9d32b4c985 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java @@ -27,8 +27,15 @@ /** * Client for connecting to App Configuration when multiple replicas are in use. + * + * The client automatically retries retryable HTTP errors (429, 408, 5xx) and implements backoff logic to prevent + * overwhelming failing replicas. */ class AppConfigurationReplicaClient { + private static final long INITIAL_BACKOFF_OFFSET_MS = 1L; + + /** HTTP status code for "Not Modified" responses */ + private static final int HTTP_NOT_MODIFIED = 304; private final String endpoint; @@ -49,7 +56,7 @@ class AppConfigurationReplicaClient { this.endpoint = endpoint; this.originClient = originClient; this.client = client; - this.backoffEndTime = Instant.now().minusMillis(1); + this.backoffEndTime = Instant.now().minusMillis(INITIAL_BACKOFF_OFFSET_MS); this.failedAttempts = 0; } @@ -96,6 +103,7 @@ String getOriginClient() { * * @param key String value of the watch key * @param label String value of the watch key, use \0 for null. + * @param context Azure SDK context for request correlation * @return The first returned configuration. */ ConfigurationSetting getWatchKey(String key, String label, Context context) @@ -108,7 +116,7 @@ ConfigurationSetting getWatchKey(String key, String label, Context context) this.failedAttempts = 0; return watchKey; } catch (HttpResponseException e) { - throw hanndleHttpResponseException(e); + throw handleHttpResponseException(e); } catch (UncheckedIOException e) { throw new AppConfigurationStatusException(e.getMessage(), null, null); } @@ -118,7 +126,9 @@ ConfigurationSetting getWatchKey(String key, String label, Context context) * Gets a list of Configuration Settings from the given config store that match the Setting Selector criteria. * * @param settingSelector Information on which setting to pull. i.e. number of results, key value... + * @param context Azure SDK context for request correlation * @return List of Configuration Settings. + * @throws HttpResponseException if the request fails */ List listSettings(SettingSelector settingSelector, Context context) throws HttpResponseException { @@ -132,12 +142,20 @@ List listSettings(SettingSelector settingSelector, Context this.failedAttempts = 0; return configurationSettings; } catch (HttpResponseException e) { - throw hanndleHttpResponseException(e); + throw handleHttpResponseException(e); } catch (UncheckedIOException e) { throw new AppConfigurationStatusException(e.getMessage(), null, null); } } + /** + * Lists feature flags from the Azure App Configuration store. + * + * @param settingSelector selector criteria for feature flags + * @param context Azure SDK context for request correlation + * @return FeatureFlags containing the retrieved feature flags and match conditions + * @throws HttpResponseException if the request fails + */ FeatureFlags listFeatureFlags(SettingSelector settingSelector, Context context) throws HttpResponseException { List configurationSettings = new ArrayList<>(); @@ -157,12 +175,21 @@ FeatureFlags listFeatureFlags(SettingSelector settingSelector, Context context) settingSelector.setMatchConditions(checks); return new FeatureFlags(settingSelector, configurationSettings); } catch (HttpResponseException e) { - throw hanndleHttpResponseException(e); + throw handleHttpResponseException(e); } catch (UncheckedIOException e) { throw new AppConfigurationStatusException(e.getMessage(), null, null); } } + /** + * Lists configuration settings from a specific snapshot. + * + * @param snapshotName the name of the snapshot to retrieve settings from + * @param context Azure SDK context for request correlation + * @return list of configuration settings from the snapshot + * @throws IllegalArgumentException if the snapshot is not of type KEY + * @throws HttpResponseException if the request fails + */ List listSettingSnapshot(String snapshotName, Context context) { List configurationSettings = new ArrayList<>(); try { @@ -178,7 +205,7 @@ List listSettingSnapshot(String snapshotName, Context cont settings.forEach(setting -> configurationSettings.add(NormalizeNull.normalizeNullLabel(setting))); return configurationSettings; } catch (HttpResponseException e) { - throw hanndleHttpResponseException(e); + throw handleHttpResponseException(e); } catch (UncheckedIOException e) { throw new AppConfigurationStatusException(e.getMessage(), null, null); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java index 1aef5729e617..6c6221db85ca 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java @@ -11,19 +11,22 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; /** - * Manages all client connections for all configuration stores. + * Manages all client connections for all configuration stores with support for replica failover. */ public class AppConfigurationReplicaClientFactory { + /** Map of connection managers keyed by origin endpoint */ private static final Map CONNECTIONS = new HashMap<>(); + /** List of configured stores for endpoint resolution */ private final List configStores; /** - * Sets up Connections to all configuration stores. + * Sets up connections to all configuration stores with replica support. * - * @param clientBuilder builder for app configuration replica clients - * @param configStores configuration info for config stores + * @param clientBuilder builder for creating app configuration replica clients + * @param configStores configuration information for all config stores + * @param replicaLookUp service for discovering and managing replica endpoints */ AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder, List configStores, ReplicaLookUp replicaLookUp) { @@ -37,42 +40,47 @@ public class AppConfigurationReplicaClientFactory { } /** - * @return the connections + * Gets all connection managers mapped by their origin endpoints. + * + * @return map of endpoint to connection manager */ public Map getConnections() { return CONNECTIONS; } /** - * Returns the current used endpoint for a given config store. - * @param originEndpoint identifier of the store. The identifier is the primary endpoint of the store. - * @return ConfigurationClient for accessing App Configuration + * Returns available replica clients for a given configuration store. + * */ List getAvailableClients(String originEndpoint) { return CONNECTIONS.get(originEndpoint).getAvailableClients(); } /** - * Returns the current used endpoint for a given config store. - * @param originEndpoint identifier of the store. The identifier is the primary endpoint of the store. - * @return ConfigurationClient for accessing App Configuration + * Returns available replica clients for a given configuration store with current client preference. + * + * @param originEndpoint identifier of the store (primary endpoint) + * @param useCurrent whether to prefer the currently active client + * @return list of available replica clients for the store */ List getAvailableClients(String originEndpoint, Boolean useCurrent) { return CONNECTIONS.get(originEndpoint).getAvailableClients(useCurrent); } /** - * Sets backoff time for the current client that is being used, and attempts to get a new one. - * @param originEndpoint identifier of the store. The identifier is the primary endpoint of the store. - * @param endpoint replica endpoint + * Sets backoff time for a specific replica client due to connection failure. + * + * @param originEndpoint identifier of the store (primary endpoint) + * @param endpoint the specific replica endpoint that failed */ void backoffClientClient(String originEndpoint, String endpoint) { CONNECTIONS.get(originEndpoint).backoffClient(endpoint); } /** - * Gets the health of the client connections to App Configuration - * @return map of endpoint origin it's health + * Gets the health status of all managed configuration store connections. + * + * @return map of origin endpoint to health status */ Map getHealth() { Map health = new HashMap<>(); @@ -83,10 +91,10 @@ Map getHealth() { } /** - * Returns the origin endpoint for a given endpoint. If not found will return the given endpoint; + * Finds the origin endpoint for a given replica endpoint. * - * @param endpoint App Configuration Endpoint - * @return String Endpoint + * @param endpoint the replica endpoint to find the origin for + * @return the origin endpoint, or the input endpoint if no mapping is found */ String findOriginForEndpoint(String endpoint) { for (ConfigStore store : configStores) { @@ -100,14 +108,22 @@ String findOriginForEndpoint(String endpoint) { } /** - * Sets the replica as the currently used endpoint for connecting to the config store. - * @param originEndpoint Origin Configuration Store - * @param replicaEndpoint Replica that was last successfully connected to. + * Sets the current active replica for a configuration store. + * + * @param originEndpoint the origin configuration store endpoint + * @param replicaEndpoint the replica endpoint that was successfully connected to */ void setCurrentConfigStoreClient(String originEndpoint, String replicaEndpoint) { CONNECTIONS.get(originEndpoint).setCurrentClient(replicaEndpoint); } + /** + * Updates the sync token for a specific replica endpoint. + * + * @param originEndpoint the origin configuration store endpoint + * @param endpoint the specific replica endpoint + * @param syncToken the new sync token to store + */ void updateSyncToken(String originEndpoint, String endpoint, String syncToken) { CONNECTIONS.get(originEndpoint).updateSyncToken(endpoint, syncToken); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java index c018efb0566d..ce848571356e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java @@ -28,36 +28,82 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector; +/** + * Azure App Configuration data loader implementation for Spring Boot's ConfigDataLoader. + * + * @since 6.0.0 + */ + public class AzureAppConfigDataLoader implements ConfigDataLoader { + /** + * Logger instance for this class. + */ private static Log logger = new DeferredLog(); + /** + * The Azure App Configuration data resource being processed. + */ private AzureAppConfigDataResource resource; + /** + * Factory for creating replica clients to connect to Azure App Configuration. + */ private AppConfigurationReplicaClientFactory replicaClientFactory; + /** + * Factory for creating Key Vault clients for secret resolution. + */ private AppConfigurationKeyVaultClientFactory keyVaultClientFactory; + /** + * State holder for managing configuration and feature flag states. + */ private StateHolder storeState = new StateHolder(); + /** + * Client for handling feature flag operations. + */ private FeatureFlagClient featureFlagClient; + /** + * Request context for tracking operations and telemetry. + */ private Context requestContext; + /** + * Application start time for calculating delays. + */ private static final Instant START_DATE = Instant.now(); + /** + * Pre-kill time in seconds for delaying exceptions during startup. + */ private static final Integer PREKILL_TIME = 5; + /** + * Constructs a new AzureAppConfigDataLoader with the specified logger factory. + * + * @param logFactory the deferred log factory for creating loggers + */ public AzureAppConfigDataLoader(DeferredLogFactory logFactory) { logger = logFactory.getLog(getClass()); } + /** + * Loads configuration data from Azure App Configuration service. + * + * @param context the config data loader context + * @param resource the Azure App Configuration data resource + * @return ConfigData containing loaded property sources + * @throws IOException if an I/O error occurs during loading + * @throws ConfigDataResourceNotFoundException if the configuration resource is not found + */ @Override public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResource resource) throws IOException, ConfigDataResourceNotFoundException { this.resource = resource; storeState.setNextForcedRefresh(resource.getRefreshInterval()); - if (context.getBootstrapContext().isRegistered(FeatureFlagClient.class)) { featureFlagClient = context.getBootstrapContext().get(FeatureFlagClient.class); } else { @@ -67,9 +113,7 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour } // Reset telemetry usage for refresh featureFlagClient.resetTelemetry(); - List> sourceList = new ArrayList<>(); - if (resource.isConfigStoreEnabled()) { replicaClientFactory = context.getBootstrapContext() .get(AppConfigurationReplicaClientFactory.class); @@ -80,7 +124,6 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour .getAvailableClients(resource.getEndpoint(), true); boolean reloadFailed = false; - boolean pushRefresh = false; PushNotification notification = resource.getMonitoring().getPushNotification(); if ((notification.getPrimaryToken() != null @@ -148,9 +191,15 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour return new ConfigData(sourceList); } + /** + * Handles failed property source generation when all replicas fail during application startup. + * + * @param e the exception that caused the failure + * @throws RuntimeException always thrown to indicate the startup failure + */ private void failedToGeneratePropertySource(Exception e) { - logger.error("Fail fast is set and there was an error reading configuration from Azure App " - + "Configuration store " + resource.getEndpoint() + "."); + logger.error("Configuration loading failed during application startup from Azure App Configuration store " + + resource.getEndpoint() + ". Application cannot start without required configuration."); delayException(); throw new RuntimeException("Failed to generate property sources for " + resource.getEndpoint(), e); } @@ -187,11 +236,11 @@ private List createSettings(AppConfigurationRepl } /** - * Creates a new set of AppConfigurationPropertySources, 1 per Label. + * Creates a list of feature flags from Azure App Configuration. * * @param client client for connecting to App Configuration - * @return a list of AppConfigurationPropertySources - * @throws Exception creating a property source failed + * @return a list of FeatureFlags + * @throws Exception creating feature flags failed */ private List createFeatureFlags(AppConfigurationReplicaClient client) throws Exception { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java index 8c5d1cabd38e..927b63d9ec72 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java @@ -27,15 +27,30 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; +/** + * Resolves Azure App Configuration data locations for Spring Boot's ConfigData API. + * + * @since 6.0.0 + */ + public class AzureAppConfigDataLocationResolver implements ConfigDataLocationResolver { private static final Log LOGGER = new DeferredLog(); + /** Prefix used to identify Azure App Configuration locations */ public static final String PREFIX = "azureAppConfiguration"; + /** Flag to track startup phase for proper resource initialization */ private static final AtomicBoolean START_UP = new AtomicBoolean(true); + /** + * Determines if the given location can be resolved by this resolver. + * + * @param context the resolver context containing binder and bootstrap information + * @param location the configuration data location to check + * @return true if this resolver can handle the location, false otherwise + */ @Override public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { if (!location.hasPrefix(PREFIX)) { @@ -57,6 +72,15 @@ public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDat return (hasEndpoint || hasConnectionString || hasEndpoints || hasConnectionStrings); } + } + + /** + * Resolves configuration data resources for the given location. + * + * @param context the resolver context + * @param location the configuration data location + * @return empty list of resources + */ @Override public List resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) @@ -64,6 +88,15 @@ public List resolve(ConfigDataLocationResolverContex return Collections.emptyList(); } + /** + * Resolves profile-specific configuration data resources. + * + * @param resolverContext the resolver context + * @param location the configuration data location + * @param profiles the active Spring profiles + * @return list of Azure App Configuration data resources + * @throws ConfigDataLocationNotFoundException if location cannot be found + */ @Override public List resolveProfileSpecific( ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles) @@ -80,6 +113,12 @@ public List resolveProfileSpecific( return locations; } + /** + * Loads and validates Azure App Configuration properties from the configuration context. + * + * @param context the configuration data location resolver context + * @return validated Azure App Configuration properties + */ protected AppConfigurationProperties loadProperties(ConfigDataLocationResolverContext context) { Binder binder = context.getBinder(); BindHandler bindHandler = getBindHandler(context); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java index 3f31ee7fd926..b59937ca3b15 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java @@ -14,26 +14,48 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector; +/** + * Represents an Azure App Configuration data resource that extends Spring Boot's ConfigDataResource. + * + * @since 6.0.0 + */ public class AzureAppConfigDataResource extends ConfigDataResource { + /** Indicates whether the configuration store is enabled for loading configuration data. */ private final boolean configStoreEnabled; + /** The endpoint URL of the Azure App Configuration store. */ private final String endpoint; - private List trimKeyPrefix; + /** List of key prefixes to trim from configuration keys when loading. */ + private final List trimKeyPrefix; + /** Spring Boot profiles configuration for conditional property loading. */ private final Profiles profiles; - private List selects = new ArrayList<>(); + /** List of selectors for filtering key-value pairs from the configuration store. */ + private final List selects; - private List featureFlagSelects = new ArrayList<>(); + /** List of selectors for filtering feature flag key-value pairs from the configuration store. */ + private final List featureFlagSelects; + /** Monitoring configuration for the configuration store including refresh triggers. */ private final AppConfigurationStoreMonitoring monitoring; + /** Indicates whether this resource supports configuration refresh at runtime. */ private final boolean isRefresh; - private Duration refreshInterval; + /** The interval at which configuration should be refreshed from the store. */ + private final Duration refreshInterval; + /** + * Constructs a new AzureAppConfigDataResource with the specified configuration store settings. + * + * @param configStore the configuration store settings containing endpoint, selectors, and other options + * @param profiles the Spring Boot profiles for conditional configuration loading + * @param isRefresh whether this resource supports runtime configuration refresh + * @param refreshInterval the interval at which configuration should be refreshed + */ AzureAppConfigDataResource(ConfigStore configStore, Profiles profiles, boolean isRefresh, Duration refreshInterval) { this.configStoreEnabled = configStore.isEnabled(); @@ -48,93 +70,83 @@ public class AzureAppConfigDataResource extends ConfigDataResource { } /** - * @return the selects + * Gets the list of key-value selectors used to filter configuration data from the store. + * + * @return the list of configuration key-value selectors, may be null or empty */ public List getSelects() { return selects; } /** - * @param selects the selects to set - */ - public void setSelects(List selects) { - this.selects = selects; - } - - /** - * @return the selects for feature flags + * Gets the list of feature flag selectors used to filter feature flag data from the store. + * + * @return the list of feature flag selectors, may be null or empty */ public List getFeatureFlagSelects() { return featureFlagSelects; } /** - * @param featureFlagSelects the selects to set - */ - public void setFeatureFlagSelects(List featureFlagSelects) { - this.featureFlagSelects = featureFlagSelects; - } - - /** - * @return the configStoreEnabled + * Checks whether the configuration store is enabled for loading configuration data. + * + * @return true if the configuration store is enabled, false otherwise */ public boolean isConfigStoreEnabled() { return configStoreEnabled; } /** - * @return the endpoint + * Gets the endpoint URL of the Azure App Configuration store. + * + * @return the endpoint URL as a string, may be null if not configured */ public String getEndpoint() { return endpoint; } /** - * @return the monitoring + * Gets the monitoring configuration for this configuration store. + * + * @return the monitoring configuration, may be null if not configured */ public AppConfigurationStoreMonitoring getMonitoring() { return monitoring; } /** - * @return the trimKeyPrefix + * Gets the list of key prefixes to trim from configuration keys when loading. + * + * @return the list of key prefixes to trim, may be null or empty if no trimming is configured */ public List getTrimKeyPrefix() { return trimKeyPrefix; } /** - * @param trimKeyPrefix the trimKeyPrefix to set - */ - public void setTrimKeyPrefix(List trimKeyPrefix) { - this.trimKeyPrefix = trimKeyPrefix; - } - - /** - * @return the profiles + * Gets the Spring Boot profiles configuration for conditional property loading. + * + * @return the profiles configuration, never null */ public Profiles getProfiles() { return profiles; } /** - * @return the isRefresh + * Returns if true if this resource is being refreshed. False if the resource is being loaded at startup. + * + * @return true if this is a refresh operation, false if it is a startup load */ public boolean isRefresh() { return isRefresh; } /** - * @return the refreshInterval + * Gets the interval at which configuration should be refreshed from the store. + * + * @return the refresh interval, may be null if not configured */ public Duration getRefreshInterval() { return refreshInterval; } - - /** - * @param refreshInterval the refreshInterval to set - */ - public void setRefreshInterval(Duration refreshInterval) { - this.refreshInterval = refreshInterval; - } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java index 073691bd9477..ecedfbbc2a7c 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java @@ -5,14 +5,26 @@ import java.util.Random; /** - * Calculates the amount of time to the next refresh, if a refresh fails. + * Utility class for calculating exponential backoff times for Azure App Configuration retry operations. */ final class BackoffTimeCalculator { - private static final Long MAX_ATTEMPTS = (long) 63; + /** + * Maximum number of attempts to consider for exponential backoff calculation. This prevents integer overflow when + * calculating 2^attempts. Value of 63 ensures that 2^63 is the largest power of 2 that fits in a long. + */ + private static final long MAX_ATTEMPTS = 63; - private static final Long SECONDS_TO_NANO_SECONDS = (long) 1000000000; + /** + * Conversion factor from seconds to nanoseconds. Used to convert backoff times from seconds to nanoseconds for + * precise timing. + */ + private static final long SECONDS_TO_NANOSECONDS = 1_000_000_000L; + /** + * Generator for introducing jitter in backoff calculations. Jitter helps prevent multiple + * clients from retrying simultaneously (thundering herd). + */ private static final Random RANDOM = new Random(); private static Long maxBackoff = (long) 600; diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java index 17124ea72904..041e114884d2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java @@ -18,33 +18,43 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore; /** - * Holds a set of connections to an app configuration store with zero to many geo-replications. + * Manages connection pools and client lifecycle for Azure App Configuration stores with support for geo-replication, + * auto-failover, and intelligent client routing. */ class ConnectionManager { private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class); + /** The primary endpoint URL for the App Configuration store. */ private final String originEndpoint; - // Used if multiple connection method is given. + /** List of configured replica clients for the primary App Configuration store. */ private List clients; - private Map autoFailoverClients; + /** Map of auto-discovered failover clients, keyed by endpoint URL. */ + private final Map autoFailoverClients; + /** Currently active replica endpoint being used for requests. */ private String currentReplica; + /** Current health status of the App Configuration store connection. */ private AppConfigurationStoreHealth health; + /** Builder for creating App Configuration replica clients. */ private final AppConfigurationReplicaClientsBuilder clientBuilder; + /** Configuration store settings and connection parameters. */ private final ConfigStore configStore; + /** Service for discovering auto-failover replica endpoints. */ private final ReplicaLookUp replicaLookUp; /** - * Creates a set of connections to an app configuration store. - * @param clientBuilder Builder for App Configuration Clients - * @param configStore Connection info for the store + * Creates a connection manager for the specified App Configuration store. + * + * @param clientBuilder the builder for creating App Configuration replica clients; must not be null + * @param configStore the configuration store settings and connection parameters; must not be null + * @param replicaLookUp the service for discovering auto-failover endpoints; must not be null */ ConnectionManager(AppConfigurationReplicaClientsBuilder clientBuilder, ConfigStore configStore, ReplicaLookUp replicaLookUp) { @@ -58,35 +68,47 @@ class ConnectionManager { } /** - * Gets the current health information on the Connection to the Config Store - * @return AppConfigurationConfigStoreHealth + * Retrieves the current health status of the App Configuration store connection. + * + * @return the current health status; never null */ AppConfigurationStoreHealth getHealth() { return this.health; } + /** + * Sets the current active replica endpoint for client routing. + * + * @param replicaEndpoint the endpoint URL to set as current; may be null to reset to primary endpoint + */ void setCurrentClient(String replicaEndpoint) { this.currentReplica = replicaEndpoint; } /** - * @return the originEndpoint + * Retrieves the primary (origin) endpoint URL for the App Configuration store. + * + * @return the primary endpoint URL; never null */ String getMainEndpoint() { return originEndpoint; } /** - * Returns a client. - * @return ConfigurationClient + * Retrieves all available App Configuration clients that are ready for use. + * + * @return a list of available clients; may be empty if all clients are currently unavailable */ List getAvailableClients() { return getAvailableClients(false); } /** - * Returns a client. - * @return ConfigurationClient + * Retrieves available App Configuration clients with optional current replica preference. + * + * @param useCurrent if true, prioritizes returning clients starting from the current replica; if false, returns all + * available clients + * @return a list of available clients ordered by preference; may be empty if all clients are currently unavailable */ List getAvailableClients(Boolean useCurrent) { if (clients == null) { @@ -138,13 +160,12 @@ List getAvailableClients(Boolean useCurrent) { } else if (clients.size() > 0) { this.health = AppConfigurationStoreHealth.UP; } - - return availableClients; } /** - * Call when the current client failed - * @param endpoint replica endpoint + * Applies exponential backoff to a failed client endpoint. + * + * @param endpoint the endpoint URL of the failed client; must not be null or empty */ void backoffClient(String endpoint) { for (AppConfigurationReplicaClient client : clients) { @@ -173,10 +194,20 @@ void updateSyncToken(String endpoint, String syncToken) { .ifPresent(client -> client.updateSyncToken(syncToken)); } + /** + * Retrieves the monitoring configuration for the App Configuration store. + * + * @return the monitoring configuration; may be null if not configured + */ AppConfigurationStoreMonitoring getMonitoring() { return configStore.getMonitoring(); } + /** + * Retrieves the feature flag store configuration. + * + * @return the feature flag store configuration; may be null if not configured + */ FeatureFlagStore getFeatureFlagStore() { return configStore.getFeatureFlags(); } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 20e965d4e3ea..8bf39dd1115c 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -74,7 +74,8 @@ public class FeatureManager { * @param featureManagementConfigurations Configuration Properties for Feature Flags * @param properties FeatureManagementConfigProperties * @param contextAccessor TargetingContextAccessor - * @param evaluationOptions TargetingE + * @param evaluationOptions TargetingEvaluationOptions + * @param telemetryPublisher TelemetryPublisher */ FeatureManager(ApplicationContext context, FeatureManagementProperties featureManagementConfigurations, FeatureManagementConfigProperties properties, TargetingContextAccessor contextAccessor, diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java index fd52e85912ee..0d25d4508272 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java @@ -21,10 +21,12 @@ public static void updateValueFromMapToList(Map parameters, Stri } /** - * Looks at the given key in the parameters and coverts it to a list if it is currently a map. + * Looks at the given key in the parameters and converts it to a list if it is currently a map. Also handles empty + * strings and null values based on the fixNull parameter. * * @param parameters map of generic objects - * @param key key of object int the parameters map + * @param key key of object in the parameters map + * @param fixNull whether to convert null values to empty lists */ @SuppressWarnings("unchecked") public static void updateValueFromMapToList(Map parameters, String key, boolean fixNull) { @@ -47,7 +49,8 @@ public static String getKeyCase(Map parameters, String key) { } /** - * Computes the percentage that the contextId falls into. + * Computes the percentage bucket (0-100) that the contextId falls into. Uses SHA-256 hash for consistent + * distribution across the percentage range. * * @param contextId Id of the context being targeted * @return the bucket value of the context id