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 d25e616ced98..a7e4d24d4ae3 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
@@ -17,8 +17,8 @@
import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlagState;
-import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore;
@@ -30,6 +30,18 @@ public class AppConfigurationRefreshUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationRefreshUtil.class);
+ private static final String FEATURE_FLAG_PREFIX = ".appconfig.featureflag/*";
+
+ /**
+ * Functional interface for refresh operations that can throw AppConfigurationStatusException.
+ */
+ @FunctionalInterface
+ private interface RefreshOperation {
+
+ void execute(AppConfigurationReplicaClient client, RefreshEventData eventData, Context context)
+ throws AppConfigurationStatusException;
+ }
+
/**
* Checks all configured stores to determine if any configurations need to be refreshed.
*
@@ -67,66 +79,46 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
if ((notification.getPrimaryToken() != null
&& StringUtils.hasText(notification.getPrimaryToken().getName()))
|| (notification.getSecondaryToken() != null
- && StringUtils.hasText(notification.getPrimaryToken().getName()))) {
+ && StringUtils.hasText(notification.getSecondaryToken().getName()))) {
pushRefresh = true;
}
Context context = new Context("refresh", true).addData(PUSH_REFRESH, pushRefresh);
-
- clientFactory.findActiveClients(originEndpoint);
- AppConfigurationReplicaClient client = clientFactory.getNextActiveClient(originEndpoint, false);
+ clientFactory.findActiveClients(originEndpoint);
if (monitor.isEnabled() && StateHolder.getLoadState(originEndpoint)) {
- while (client != null) {
- try {
- refreshWithTime(client, StateHolder.getState(originEndpoint), monitor.getRefreshInterval(),
- eventData, replicaLookUp, context);
- if (eventData.getDoRefresh()) {
- clientFactory.setCurrentConfigStoreClient(originEndpoint, client.getEndpoint());
- return eventData;
- }
- // If check didn't throw an error other clients don't need to be checked.
- break;
- } catch (HttpResponseException e) {
- LOGGER.warn(
- "Failed to connect to App Configuration store {} during configuration refresh check. "
- + "Status: {}, Message: {}",
- client.getEndpoint(), e.getResponse().getStatusCode(), e.getMessage());
-
- clientFactory.backoffClient(originEndpoint, client.getEndpoint());
- client = clientFactory.getNextActiveClient(originEndpoint, false);
- }
+ RefreshEventData result = executeRefreshWithRetry(
+ clientFactory,
+ originEndpoint,
+ (client, data, ctx) -> refreshWithTime(client, StateHolder.getState(originEndpoint),
+ monitor.getRefreshInterval(), data, replicaLookUp, ctx),
+ eventData,
+ context,
+ "configuration refresh check");
+ if (result != null) {
+ return result;
}
} else {
- LOGGER.debug("Skipping configuration refresh check for " + originEndpoint);
+ LOGGER.debug("Skipping configuration refresh check for {}", originEndpoint);
}
FeatureFlagStore featureStore = connection.getFeatureFlagStore();
if (featureStore.getEnabled() && StateHolder.getStateFeatureFlag(originEndpoint) != null) {
- client = clientFactory.getNextActiveClient(originEndpoint, false);
- while (client != null) {
- try {
- refreshWithTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(originEndpoint),
- monitor.getFeatureFlagRefreshInterval(), eventData, replicaLookUp, context);
- if (eventData.getDoRefresh()) {
- clientFactory.setCurrentConfigStoreClient(originEndpoint, client.getEndpoint());
- return eventData;
- }
- // If check didn't throw an error other clients don't need to be checked.
- break;
- } catch (HttpResponseException e) {
- LOGGER.warn(
- "Failed to connect to App Configuration store {} during feature flag refresh check. "
- + "Status: {}, Message: {}",
- client.getEndpoint(), e.getResponse().getStatusCode(), e.getMessage());
-
- clientFactory.backoffClient(originEndpoint, client.getEndpoint());
- client = clientFactory.getNextActiveClient(originEndpoint, false);
- }
+ RefreshEventData result = executeRefreshWithRetry(
+ clientFactory,
+ originEndpoint,
+ (client, data, ctx) -> refreshWithTimeFeatureFlags(client,
+ StateHolder.getStateFeatureFlag(originEndpoint),
+ monitor.getFeatureFlagRefreshInterval(), data, replicaLookUp, ctx),
+ eventData,
+ context,
+ "feature flag refresh check");
+ if (result != null) {
+ return result;
}
} else {
- LOGGER.debug("Skipping feature flag refresh check for " + originEndpoint);
+ LOGGER.debug("Skipping feature flag refresh check for {}", originEndpoint);
}
}
@@ -138,6 +130,47 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
return eventData;
}
+ /**
+ * Executes a refresh operation with automatic retry logic across replica clients.
+ *
+ * @param clientFactory factory for accessing App Configuration clients
+ * @param originEndpoint the endpoint of the origin configuration store
+ * @param operation the refresh operation to execute
+ * @param eventData the refresh event data to update
+ * @param context the operation context
+ * @param checkType description of the check type for logging (e.g., "configuration refresh check")
+ * @return the eventData if refresh is needed, null otherwise
+ */
+ private RefreshEventData executeRefreshWithRetry(
+ AppConfigurationReplicaClientFactory clientFactory,
+ String originEndpoint,
+ RefreshOperation operation,
+ RefreshEventData eventData,
+ Context context,
+ String checkType) {
+ AppConfigurationReplicaClient client = clientFactory.getNextActiveClient(originEndpoint, false);
+
+ while (client != null) {
+ try {
+ operation.execute(client, eventData, context);
+ if (eventData.getDoRefresh()) {
+ clientFactory.setCurrentConfigStoreClient(originEndpoint, client.getEndpoint());
+ return eventData;
+ }
+ // If check didn't throw an error, other clients don't need to be checked.
+ break;
+ } catch (HttpResponseException e) {
+ LOGGER.warn(
+ "Failed to connect to App Configuration store {} during {}. Status: {}, Message: {}",
+ client.getEndpoint(), checkType, e.getResponse().getStatusCode(), e.getMessage());
+
+ clientFactory.backoffClient(originEndpoint, client.getEndpoint());
+ client = clientFactory.getNextActiveClient(originEndpoint, false);
+ }
+ }
+ return null;
+ }
+
/**
* Performs a refresh check for a specific store client without time constraints. This method is used for refresh
* failure scenarios only.
@@ -158,7 +191,7 @@ static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String or
/**
* Performs a feature flag refresh check for a specific store client. This method is used for refresh failure
* scenarios only.
- *
+ *
* @param featureStoreEnabled whether feature store is enabled
* @param client the client for checking refresh status
* @param context the operation context
@@ -172,7 +205,7 @@ static boolean refreshStoreFeatureFlagCheck(Boolean featureStoreEnabled,
if (featureStoreEnabled && StateHolder.getStateFeatureFlag(endpoint) != null) {
refreshWithoutTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(endpoint), eventData, context);
} else {
- LOGGER.debug("Skipping feature flag refresh check for " + endpoint);
+ LOGGER.debug("Skipping feature flag refresh check for {}", endpoint);
}
return eventData.getDoRefresh();
}
@@ -194,7 +227,15 @@ private static void refreshWithTime(AppConfigurationReplicaClient client, State
throws AppConfigurationStatusException {
if (Instant.now().isAfter(state.getNextRefreshCheck())) {
replicaLookUp.updateAutoFailoverEndpoints();
- refreshWithoutTime(client, state.getWatchKeys(), eventData, context);
+
+ // Check watched configuration settings first if configured
+ List watchedSettings = state.getCollectionWatchKeys();
+ if (watchedSettings != null && !watchedSettings.isEmpty()) {
+ refreshWithoutTimeWatchedConfigurationSettings(client, watchedSettings, eventData, context);
+ } else {
+ // Fall back to traditional watch key monitoring
+ refreshWithoutTime(client, state.getWatchKeys(), eventData, context);
+ }
StateHolder.getCurrentState().updateStateRefresh(state, refreshInterval);
}
@@ -226,6 +267,34 @@ private static void refreshWithoutTime(AppConfigurationReplicaClient client, Lis
}
}
+ /**
+ * Checks configuration watched configuration settings for etag changes without time validation. This method
+ * immediately checks all watched configuration settings selectors for changes regardless of refresh intervals.
+ *
+ * @param client the App Configuration client to use for checking
+ * @param watchedSettings the list of watched 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 refreshWithoutTimeWatchedConfigurationSettings(AppConfigurationReplicaClient client,
+ List watchedSettings, RefreshEventData eventData, Context context)
+ throws AppConfigurationStatusException {
+ for (WatchedConfigurationSettings watchedSetting : watchedSettings) {
+ if (client.checkWatchKeys(watchedSetting.getSettingSelector(), context)) {
+ String eventDataInfo = watchedSetting.getSettingSelector().getKeyFilter();
+
+ // Only one refresh event needs to be called to update all of the
+ // stores, not one for each.
+ LOGGER.info("Configuration Refresh Event triggered by watched configuration settings: {}",
+ eventDataInfo);
+
+ eventData.setMessage(eventDataInfo);
+ return;
+ }
+ }
+ }
+
/**
* Checks feature flag refresh triggers with time-based validation. Only performs the refresh check if the refresh
* interval has elapsed.
@@ -245,15 +314,13 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl
if (date.isAfter(state.getNextRefreshCheck())) {
replicaLookUp.updateAutoFailoverEndpoints();
- for (FeatureFlags featureFlags : state.getWatchKeys()) {
+ for (WatchedConfigurationSettings featureFlags : state.getWatchKeys()) {
if (client.checkWatchKeys(featureFlags.getSettingSelector(), context)) {
- String eventDataInfo = ".appconfig.featureflag/*";
-
- // Only one refresh Event needs to be call to update all of the
+ // Only one refresh event needs to be called to update all of the
// stores, not one for each.
- LOGGER.info("Configuration Refresh Event triggered by " + eventDataInfo);
+ LOGGER.info("Configuration Refresh Event triggered by {}", FEATURE_FLAG_PREFIX);
- eventData.setMessage(eventDataInfo);
+ eventData.setMessage(FEATURE_FLAG_PREFIX);
return;
}
@@ -276,15 +343,13 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl
private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagState watchKeys,
RefreshEventData eventData, Context context) throws AppConfigurationStatusException {
- for (FeatureFlags featureFlags : watchKeys.getWatchKeys()) {
+ for (WatchedConfigurationSettings featureFlags : watchKeys.getWatchKeys()) {
if (client.checkWatchKeys(featureFlags.getSettingSelector(), context)) {
- String eventDataInfo = ".appconfig.featureflag/*";
-
- // Only one refresh Event needs to be call to update all of the
+ // Only one refresh event needs to be called to update all of the
// stores, not one for each.
- LOGGER.info("Configuration Refresh Event triggered by " + eventDataInfo);
+ LOGGER.info("Configuration Refresh Event triggered by {}", FEATURE_FLAG_PREFIX);
- eventData.setMessage(eventDataInfo);
+ eventData.setMessage(FEATURE_FLAG_PREFIX);
}
}
@@ -292,7 +357,7 @@ 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
@@ -313,9 +378,9 @@ private static void checkETag(ConfigurationSetting watchSetting, ConfigurationSe
String eventDataInfo = watchSetting.getKey();
- // Only one refresh Event needs to be call to update all of the
+ // Only one refresh event needs to be called to update all of the
// stores, not one for each.
- LOGGER.info("Configuration Refresh Event triggered by " + eventDataInfo);
+ LOGGER.info("Configuration Refresh Event triggered by {}", eventDataInfo);
eventData.setMessage(eventDataInfo);
}
}
@@ -325,7 +390,7 @@ private static void checkETag(ConfigurationSetting watchSetting, ConfigurationSe
*/
static class RefreshEventData {
- private static final String MSG_TEMPLATE = "Some keys matching %s has been updated since last check.";
+ private static final String MSG_TEMPLATE = "Some keys matching %s have been updated since last check.";
private String message;
@@ -340,7 +405,7 @@ static class RefreshEventData {
/**
* 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
*/
@@ -351,7 +416,7 @@ RefreshEventData setMessage(String prefix) {
/**
* 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
*/
@@ -363,7 +428,7 @@ private RefreshEventData setFullMessage(String message) {
/**
* Gets the refresh event message.
- *
+ *
* @return the message describing what triggered the refresh
*/
public String getMessage() {
@@ -372,7 +437,7 @@ public String getMessage() {
/**
* Indicates whether a refresh should be performed.
- *
+ *
* @return true if a refresh is needed, false otherwise
*/
public boolean getDoRefresh() {
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 0cde712d5867..049e0ed65546 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
@@ -21,7 +21,7 @@
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.data.appconfiguration.models.SnapshotComposition;
-import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
import io.netty.handler.codec.http.HttpResponseStatus;
@@ -150,15 +150,47 @@ List listSettings(SettingSelector settingSelector, Context
}
}
+ /**
+ * Gets configuration settings using watched configuration settings. This method retrieves all settings matching the
+ * selector and captures ETags for collection-based refresh monitoring.
+ *
+ * @param settingSelector selector criteria for configuration settings
+ * @param context Azure SDK context for request correlation
+ * @return WatchedConfigurationSettings containing the retrieved configuration settings and match conditions
+ * @throws HttpResponseException if the request fails
+ */
+ WatchedConfigurationSettings loadWatchedSettings(SettingSelector settingSelector, Context context) {
+ List configurationSettings = new ArrayList<>();
+ List checks = new ArrayList<>();
+ try {
+ client.listConfigurationSettings(settingSelector, context).streamByPage().forEach(pagedResponse -> {
+ checks.add(
+ new MatchConditions().setIfNoneMatch(pagedResponse.getHeaders().getValue(HttpHeaderName.ETAG)));
+ for (ConfigurationSetting setting : pagedResponse.getValue()) {
+ configurationSettings.add(NormalizeNull.normalizeNullLabel(setting));
+ }
+ });
+
+ // Needs to happen after or we don't know if the request succeeded or failed.
+ this.failedAttempts = 0;
+ settingSelector.setMatchConditions(checks);
+ return new WatchedConfigurationSettings(settingSelector, configurationSettings);
+ } catch (HttpResponseException 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
+ * @return WatchedConfigurationSettings containing the retrieved feature flags and match conditions
* @throws HttpResponseException if the request fails
*/
- FeatureFlags listFeatureFlags(SettingSelector settingSelector, Context context)
+ WatchedConfigurationSettings listFeatureFlags(SettingSelector settingSelector, Context context)
throws HttpResponseException {
List configurationSettings = new ArrayList<>();
List checks = new ArrayList<>();
@@ -175,7 +207,7 @@ FeatureFlags listFeatureFlags(SettingSelector settingSelector, Context context)
// Needs to happen after or we don't know if the request succeeded or failed.
this.failedAttempts = 0;
settingSelector.setMatchConditions(checks);
- return new FeatureFlags(settingSelector, configurationSettings);
+ return new WatchedConfigurationSettings(settingSelector, configurationSettings);
} catch (HttpResponseException e) {
throw handleHttpResponseException(e);
} catch (UncheckedIOException e) {
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java
index e585997e05a5..8423ec60e63e 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java
@@ -10,7 +10,7 @@
import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
-import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
/**
* Azure App Configuration PropertySource unique per Store Label(Profile) combo.
@@ -49,7 +49,7 @@ final class AppConfigurationSnapshotPropertySource extends AppConfigurationAppli
public void initProperties(List trim, Context context) throws InvalidConfigurationPropertyValueException {
processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName, context), null, trim);
- FeatureFlags featureFlags = new FeatureFlags(null, featureFlagsList);
+ WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, featureFlagsList);
featureFlagClient.proccessFeatureFlags(featureFlags, replicaClient.getEndpoint());
}
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 7cd92ea3387b..bca6a57646a9 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
@@ -7,11 +7,12 @@
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
-import org.springframework.boot.bootstrap.BootstrapRegistry.InstanceSupplier;
import org.springframework.boot.context.config.ConfigData;
+import org.springframework.boot.bootstrap.BootstrapRegistry.InstanceSupplier;
import org.springframework.boot.context.config.ConfigDataLoader;
import org.springframework.boot.context.config.ConfigDataLoaderContext;
import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
@@ -22,7 +23,8 @@
import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
-import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
+import com.azure.data.appconfiguration.models.SettingSelector;
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification;
@@ -127,7 +129,7 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour
if ((notification.getPrimaryToken() != null
&& StringUtils.hasText(notification.getPrimaryToken().getName()))
|| (notification.getSecondaryToken() != null
- && StringUtils.hasText(notification.getPrimaryToken().getName()))) {
+ && StringUtils.hasText(notification.getSecondaryToken().getName()))) {
pushRefresh = true;
}
// Feature Management needs to be set in the last config store.
@@ -152,7 +154,7 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour
// Reverse in order to add Profile specific properties earlier, and last profile comes first
try {
sourceList.addAll(createSettings(currentClient));
- List featureFlags = createFeatureFlags(currentClient);
+ List featureFlags = createFeatureFlags(currentClient);
logger.debug("PropertySource context.");
AppConfigurationStoreMonitoring monitoring = resource.getMonitoring();
@@ -161,13 +163,23 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour
monitoring.getFeatureFlagRefreshInterval());
if (monitoring.isEnabled()) {
- // Setting new ETag values for Watch
- List watchKeysSettings = monitoring.getTriggers().stream()
- .map(trigger -> currentClient.getWatchKey(trigger.getKey(), trigger.getLabel(),
- requestContext))
- .toList();
-
- storeState.setState(resource.getEndpoint(), watchKeysSettings, monitoring.getRefreshInterval());
+ // Check if refreshAll is enabled - if so, use watched configuration settings
+ if (monitoring.getTriggers().size() == 0) {
+ // Use watched configuration settings for refresh
+ List watchedConfigurationSettingsList = getWatchedConfigurationSettings(
+ currentClient);
+ storeState.setState(resource.getEndpoint(), Collections.emptyList(),
+ watchedConfigurationSettingsList, monitoring.getRefreshInterval());
+ } else {
+ // Use traditional watch key monitoring
+ List watchKeysSettings = monitoring.getTriggers().stream()
+ .map(trigger -> currentClient.getWatchKey(trigger.getKey(), trigger.getLabel(),
+ requestContext))
+ .toList();
+
+ storeState.setState(resource.getEndpoint(), watchKeysSettings,
+ monitoring.getRefreshInterval());
+ }
}
storeState.setLoadState(resource.getEndpoint(), true); // Success - configuration loaded, exit loop
lastException = null;
@@ -260,16 +272,16 @@ private List createSettings(AppConfigurationRepl
* Creates a list of feature flags from Azure App Configuration.
*
* @param client client for connecting to App Configuration
- * @return a list of FeatureFlags
+ * @return a list of WatchedConfigurationSettings
* @throws Exception creating feature flags failed
*/
- private List createFeatureFlags(AppConfigurationReplicaClient client)
+ private List createFeatureFlags(AppConfigurationReplicaClient client)
throws Exception {
- List featureFlagWatchKeys = new ArrayList<>();
+ List featureFlagWatchKeys = new ArrayList<>();
List profiles = resource.getProfiles().getActive();
for (FeatureFlagKeyValueSelector selectedKeys : resource.getFeatureFlagSelects()) {
- List storesFeatureFlags = featureFlagClient.loadFeatureFlags(client,
+ List storesFeatureFlags = featureFlagClient.loadFeatureFlags(client,
selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles), requestContext);
featureFlagWatchKeys.addAll(storesFeatureFlags);
}
@@ -277,6 +289,41 @@ private List createFeatureFlags(AppConfigurationReplicaClient clie
return featureFlagWatchKeys;
}
+ /**
+ * Creates a list of watched configuration settings for configuration settings from Azure App Configuration. This is
+ * used for collection-based refresh monitoring as an alternative to individual watch keys.
+ *
+ * @param client client for connecting to App Configuration
+ * @return a list of WatchedConfigurationSettings for configuration settings
+ * @throws Exception creating watched configuration settings failed
+ */
+ private List getWatchedConfigurationSettings(AppConfigurationReplicaClient client)
+ throws Exception {
+ List watchedConfigurationSettingsList = new ArrayList<>();
+ List selects = resource.getSelects();
+ List profiles = resource.getProfiles().getActive();
+
+ for (AppConfigurationKeyValueSelector selectedKeys : selects) {
+ // Skip snapshots - they don't support watched configuration settings
+ if (StringUtils.hasText(selectedKeys.getSnapshotName())) {
+ continue;
+ }
+
+ // Create watched configuration settings for each label
+ for (String label : selectedKeys.getLabelFilter(profiles)) {
+ SettingSelector settingSelector = new SettingSelector()
+ .setKeyFilter(selectedKeys.getKeyFilter() + "*")
+ .setLabelFilter(label);
+
+ WatchedConfigurationSettings watchedConfigurationSettings = client.loadWatchedSettings(settingSelector,
+ requestContext);
+ watchedConfigurationSettingsList.add(watchedConfigurationSettings);
+ }
+ }
+
+ return watchedConfigurationSettingsList;
+ }
+
/**
* Logs a replica failure with contextual information about the failure scenario and available replicas.
*
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java
index 44cb297b5e27..85ddcf4b54ef 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java
@@ -37,7 +37,7 @@
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagFilter;
import com.azure.data.appconfiguration.models.SettingSelector;
-import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.Allocation;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.Feature;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.FeatureTelemetry;
@@ -78,9 +78,9 @@ class FeatureFlagClient {
*
*
*/
- List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, String customKeyFilter,
+ List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, String customKeyFilter,
String[] labelFilter, Context context) {
- List loadedFeatureFlags = new ArrayList<>();
+ List loadedFeatureFlags = new ArrayList<>();
String keyFilter = SELECT_ALL_FEATURE_FLAGS;
@@ -95,18 +95,15 @@ List loadFeatureFlags(AppConfigurationReplicaClient replicaClient,
SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter).setLabelFilter(label);
context.addData("FeatureFlagTracing", tracing);
- FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, context);
- loadedFeatureFlags.addAll(proccessFeatureFlags(features, replicaClient.getOriginClient()));
+ WatchedConfigurationSettings features = replicaClient.listFeatureFlags(settingSelector, context);
+ loadedFeatureFlags.add(proccessFeatureFlags(features, replicaClient.getOriginClient()));
}
return loadedFeatureFlags;
}
- List proccessFeatureFlags(FeatureFlags features, String endpoint) {
- List loadedFeatureFlags = new ArrayList<>();
- loadedFeatureFlags.add(features);
-
+ WatchedConfigurationSettings proccessFeatureFlags(WatchedConfigurationSettings features, String endpoint) {
// Reading In Features
- for (ConfigurationSetting setting : features.getFeatureFlags()) {
+ for (ConfigurationSetting setting : features.getConfigurationSettings()) {
if (setting instanceof FeatureFlagConfigurationSetting
&& FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) {
FeatureFlagConfigurationSetting featureFlag = (FeatureFlagConfigurationSetting) setting;
@@ -114,7 +111,7 @@ List proccessFeatureFlags(FeatureFlags features, String endpoint)
properties.put(featureFlag.getKey(), createFeature(featureFlag, endpoint));
}
}
- return loadedFeatureFlags;
+ return features;
}
/**
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/State.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/State.java
index 1d237001e78e..59a1b95150e9 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/State.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/State.java
@@ -6,72 +6,159 @@
import java.util.List;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
-
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
+
+/**
+ * Immutable representation of the refresh state for an Azure App Configuration store.
+ *
+ *
+ * Holds configuration watch keys, collection monitoring settings, refresh timing, and attempt tracking for a single
+ * configuration store endpoint. All fields are final to ensure thread-safety and immutability.
+ *
+ *
+ *
+ * State changes are made by creating new instances rather than mutating existing ones, following an immutable design
+ * pattern.
+ *
+ */
class State {
+ /** Configuration settings used as watch keys to trigger refresh events. */
private final List watchKeys;
+ /** Collection monitoring configurations that can trigger refresh events. */
+ private final List collectionWatchKeys;
+
+ /** The next time this store should be checked for refresh. */
private final Instant nextRefreshCheck;
+ /** The endpoint URL of the configuration store. */
private final String originEndpoint;
- private Integer refreshAttempt;
+ /** Number of refresh attempts for exponential backoff calculation. */
+ private int refreshAttempt;
+ /** The refresh interval in seconds. */
private final int refreshInterval;
+ /**
+ * Creates a new State for configuration watch keys without collection monitoring.
+ * @param watchKeys list of configuration watch keys that can trigger a refresh event
+ * @param refreshInterval refresh interval in seconds
+ * @param originEndpoint the endpoint URL of the configuration store
+ */
State(List watchKeys, int refreshInterval, String originEndpoint) {
- this.watchKeys = watchKeys;
- this.refreshInterval = refreshInterval;
- nextRefreshCheck = Instant.now().plusSeconds(refreshInterval);
- this.originEndpoint = originEndpoint;
- this.refreshAttempt = 1;
+ this(watchKeys, null, refreshInterval, originEndpoint);
}
+ /**
+ * Creates a new State with both configuration watch keys and collection monitoring. Sets the initial refresh
+ * attempt to 1 and calculates next refresh time from now.
+ * @param watchKeys list of configuration watch keys that can trigger a refresh event
+ * @param collectionWatchKeys list of collection monitoring configurations that can trigger a refresh event
+ * @param refreshInterval refresh interval in seconds
+ * @param originEndpoint the endpoint URL of the configuration store
+ */
+ State(List watchKeys, List collectionWatchKeys,
+ int refreshInterval, String originEndpoint) {
+ this(watchKeys, collectionWatchKeys, refreshInterval, originEndpoint,
+ Instant.now().plusSeconds(refreshInterval), 1);
+ }
+
+ /**
+ * Creates a new State from an existing state with an updated refresh time. Preserves the current refresh attempt
+ * count.
+ * @param oldState the existing State to copy from
+ * @param newRefresh the new refresh time
+ */
State(State oldState, Instant newRefresh) {
- this.watchKeys = oldState.getWatchKeys();
- this.refreshInterval = oldState.getRefreshInterval();
- this.nextRefreshCheck = newRefresh;
- this.originEndpoint = oldState.getOriginEndpoint();
- this.refreshAttempt = oldState.getRefreshAttempt();
+ this(oldState, newRefresh, oldState.getRefreshAttempt());
+ }
+
+ /**
+ * Creates a new State from an existing state with updated refresh time and attempt count. Used when creating states
+ * with modified refresh attempts for backoff logic.
+ * @param oldState the existing State to copy from
+ * @param newRefresh the new refresh time
+ * @param refreshAttempt the refresh attempt count
+ */
+ State(State oldState, Instant newRefresh, int refreshAttempt) {
+ this(oldState.getWatchKeys(), oldState.getCollectionWatchKeys(), oldState.getRefreshInterval(),
+ oldState.getOriginEndpoint(), newRefresh, refreshAttempt);
+ }
+
+ /**
+ * Primary constructor that initializes all fields. All other constructors delegate to this one. This constructor is
+ * private to enforce the use of the public factory-style constructors.
+ * @param watchKeys list of configuration watch keys
+ * @param collectionWatchKeys list of collection monitoring configurations (may be null)
+ * @param refreshInterval refresh interval in seconds
+ * @param originEndpoint the endpoint URL of the configuration store
+ * @param nextRefreshCheck the next time to check for refresh
+ * @param refreshAttempt the current refresh attempt count
+ */
+ private State(List watchKeys, List collectionWatchKeys,
+ int refreshInterval, String originEndpoint, Instant nextRefreshCheck, int refreshAttempt) {
+ this.watchKeys = watchKeys;
+ this.collectionWatchKeys = collectionWatchKeys;
+ this.refreshInterval = refreshInterval;
+ this.nextRefreshCheck = nextRefreshCheck;
+ this.originEndpoint = originEndpoint;
+ this.refreshAttempt = refreshAttempt;
}
/**
- * @return the watchKeys
+ * Gets the configuration settings used as watch keys for this store.
+ * @return the list of configuration watch keys
*/
public List getWatchKeys() {
return watchKeys;
}
/**
- * @return the nextRefreshCheck
+ * Gets the collection monitoring configurations for this store.
+ * @return the list of collection monitoring configurations, or null if not configured
+ */
+ public List getCollectionWatchKeys() {
+ return collectionWatchKeys;
+ }
+
+ /**
+ * Gets the next time this store should be checked for refresh.
+ * @return the Instant of the next refresh check
*/
public Instant getNextRefreshCheck() {
return nextRefreshCheck;
}
/**
- * @return the originEndpoint
+ * Gets the endpoint URL of the configuration store.
+ * @return the origin endpoint
*/
public String getOriginEndpoint() {
return originEndpoint;
}
/**
- * @return the refreshAttempt
+ * Gets the number of refresh attempts. Used for exponential backoff calculation.
+ * @return the refresh attempt count
*/
- public Integer getRefreshAttempt() {
+ public int getRefreshAttempt() {
return refreshAttempt;
}
/**
- * Adds 1 to the number of refresh attempts
+ * Increments refresh attempt count. Used when a refresh fails to track attempts for backoff logic.
+ * @return the State instance with refreshAttempt incremented by 1
*/
- public void incrementRefreshAttempt() {
+ public State incrementRefreshAttempt() {
this.refreshAttempt += 1;
+ return this;
}
/**
- * @return the refreshInterval
+ * Gets the refresh interval for this store.
+ * @return the refresh interval in seconds
*/
public int getRefreshInterval() {
return refreshInterval;
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java
index 8d63ce85ac92..e9ff389e6570 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/StateHolder.java
@@ -11,95 +11,163 @@
import java.util.concurrent.ConcurrentHashMap;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlagState;
-import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
+/**
+ * Thread-safe singleton holder for managing refresh state of Azure App Configuration stores.
+ *
+ *
+ * Maintains state for configuration settings, feature flags, and refresh intervals across multiple configuration
+ * stores. Implements exponential backoff for failed refresh attempts and coordinates the timing of refresh operations.
+ *
+ */
final class StateHolder {
+ /** Maximum jitter in seconds to add when expiring state to prevent thundering herd. */
private static final int MAX_JITTER = 15;
+ /** The current singleton instance of StateHolder. */
private static StateHolder currentState;
+ /** Map of configuration store endpoints to their refresh state. */
private final Map state = new ConcurrentHashMap<>();
+ /** Map of configuration store endpoints to their feature flag refresh state. */
private final Map featureFlagState = new ConcurrentHashMap<>();
+ /** Map tracking whether each configuration store has been successfully loaded. */
private final Map loadState = new ConcurrentHashMap<>();
+ /** Number of client-level refresh attempts for backoff calculation. */
private Integer clientRefreshAttempts = 1;
+ /** The next time a forced refresh should occur across all stores. */
private Instant nextForcedRefresh;
StateHolder() {
}
+ /**
+ * Gets the current singleton instance of StateHolder.
+ * @return the current StateHolder instance, or null if not yet initialized
+ */
static StateHolder getCurrentState() {
return currentState;
}
+ /**
+ * Updates the singleton instance to a new StateHolder.
+ * @param newState the new StateHolder instance to set as current
+ * @return the updated StateHolder instance
+ */
static StateHolder updateState(StateHolder newState) {
currentState = newState;
return currentState;
}
/**
+ * Retrieves the refresh state for a specific configuration store.
* @param originEndpoint the endpoint for the origin config store
- * @return the state
+ * @return the State for the specified store, or null if not found
*/
static State getState(String originEndpoint) {
return currentState.getFullState().get(originEndpoint);
}
+ /**
+ * Gets the full map of configuration store states.
+ * @return map of endpoint to State
+ */
private Map getFullState() {
return state;
}
+ /**
+ * Gets the full map of feature flag states.
+ * @return map of endpoint to FeatureFlagState
+ */
private Map getFullFeatureFlagState() {
return featureFlagState;
}
+ /**
+ * Gets the full map of load states.
+ * @return map of endpoint to load status
+ */
private Map getFullLoadState() {
return loadState;
}
/**
+ * Retrieves the feature flag refresh state for a specific configuration store.
* @param originEndpoint the endpoint for the origin config store
- * @return the state
+ * @return the FeatureFlagState for the specified store, or null if not found
*/
static FeatureFlagState getStateFeatureFlag(String originEndpoint) {
return currentState.getFullFeatureFlagState().get(originEndpoint);
}
/**
- * @param originEndpoint the stores origin endpoint
+ * Sets the refresh state for a configuration store.
+ * @param originEndpoint the store's origin endpoint
* @param watchKeys list of configuration watch keys that can trigger a refresh event
- * @param duration refresh duration.
+ * @param duration refresh duration
*/
void setState(String originEndpoint, List watchKeys, Duration duration) {
state.put(originEndpoint, new State(watchKeys, Math.toIntExact(duration.getSeconds()), originEndpoint));
}
/**
- * @param originEndpoint the stores origin endpoint
+ * Sets the refresh state for a configuration store with collection monitoring.
+ * @param originEndpoint the store's origin endpoint
* @param watchKeys list of configuration watch keys that can trigger a refresh event
- * @param duration refresh duration.
+ * @param collectionWatchKeys list of collection monitoring configurations that can trigger a refresh event
+ * @param duration refresh duration
+ */
+ void setState(String originEndpoint, List watchKeys,
+ List collectionWatchKeys, Duration duration) {
+ state.put(originEndpoint,
+ new State(watchKeys, collectionWatchKeys, Math.toIntExact(duration.getSeconds()), originEndpoint));
+ }
+
+ /**
+ * Sets the feature flag refresh state for a configuration store.
+ * @param originEndpoint the store's origin endpoint
+ * @param watchKeys list of feature flag watch keys that can trigger a refresh event
+ * @param duration refresh duration
*/
- void setStateFeatureFlag(String originEndpoint, List watchKeys,
+ void setStateFeatureFlag(String originEndpoint, List watchKeys,
Duration duration) {
featureFlagState.put(originEndpoint,
new FeatureFlagState(watchKeys, Math.toIntExact(duration.getSeconds()), originEndpoint));
}
+ /**
+ * Updates the configuration state with a new refresh time based on the duration.
+ * @param state the current State to update
+ * @param duration the duration to add to the current time for the next refresh
+ */
void updateStateRefresh(State state, Duration duration) {
this.state.put(state.getOriginEndpoint(),
new State(state, Instant.now().plusSeconds(Math.toIntExact(duration.getSeconds()))));
}
+ /**
+ * Updates the feature flag state with a new refresh time based on the duration.
+ * @param state the current FeatureFlagState to update
+ * @param duration the duration to add to the current time for the next refresh
+ */
void updateFeatureFlagStateRefresh(FeatureFlagState state, Duration duration) {
this.featureFlagState.put(state.getOriginEndpoint(),
new FeatureFlagState(state, Instant.now().plusSeconds(Math.toIntExact(duration.getSeconds()))));
}
+ /**
+ * Expires the state for a configuration store by setting a new refresh time with random jitter. The jitter helps
+ * prevent thundering herd when multiple stores refresh simultaneously.
+ * @param originEndpoint the endpoint of the store to expire
+ */
void expireState(String originEndpoint) {
State oldState = state.get(originEndpoint);
long wait = (long) (new SecureRandom().nextDouble() * MAX_JITTER);
@@ -111,7 +179,9 @@ void expireState(String originEndpoint) {
}
/**
- * @return the loadState
+ * Checks if a configuration store has been successfully loaded.
+ * @param originEndpoint the endpoint of the store to check
+ * @return true if the store has been loaded, false otherwise
*/
static boolean getLoadState(String originEndpoint) {
return currentState.getFullLoadState().getOrDefault(originEndpoint, false);
@@ -126,15 +196,16 @@ void setLoadState(String originEndpoint, Boolean loaded) {
}
/**
- * @return the nextForcedRefresh
+ * Gets the next time a forced refresh should occur across all stores.
+ * @return the Instant of the next forced refresh, or null if not set
*/
public static Instant getNextForcedRefresh() {
return currentState.nextForcedRefresh;
}
/**
- * Set after load or refresh is successful.
- * @param refreshPeriod the refreshPeriod to set
+ * Sets the next forced refresh time. Called after a successful load or refresh.
+ * @param refreshPeriod the duration from now until the next forced refresh; if null, no refresh is scheduled
*/
public void setNextForcedRefresh(Duration refreshPeriod) {
if (refreshPeriod != null) {
@@ -143,10 +214,11 @@ public void setNextForcedRefresh(Duration refreshPeriod) {
}
/**
- * Sets a minimum value until the next refresh. If a refresh interval has passed or is smaller than the calculated
- * backoff time, the refresh interval is set to the backoff time.
- * @param refreshInterval period between refresh checks.
- * @param defaultMinBackoff min backoff between checks
+ * Updates the next refresh time for all stores using exponential backoff on failures. Sets a minimum value until
+ * the next refresh. If a refresh interval has passed or is smaller than the calculated backoff time, the refresh
+ * interval is set to the backoff time. This prevents excessive refresh attempts during transient failures.
+ * @param refreshInterval period between refresh checks
+ * @param defaultMinBackoff minimum backoff duration between checks in seconds
*/
void updateNextRefreshTime(Duration refreshInterval, Long defaultMinBackoff) {
if (refreshInterval != null) {
@@ -160,14 +232,16 @@ void updateNextRefreshTime(Duration refreshInterval, Long defaultMinBackoff) {
}
for (Entry entry : state.entrySet()) {
- State state = entry.getValue();
- Instant newRefresh = getNextRefreshCheck(state.getNextRefreshCheck(),
- state.getRefreshAttempt(), (long) state.getRefreshInterval(), defaultMinBackoff);
-
- if (newRefresh.compareTo(entry.getValue().getNextRefreshCheck()) != 0) {
- state.incrementRefreshAttempt();
+ State storeState = entry.getValue();
+ Instant newRefresh = getNextRefreshCheck(storeState.getNextRefreshCheck(),
+ storeState.getRefreshAttempt(), (long) storeState.getRefreshInterval(), defaultMinBackoff);
+
+ State updatedState;
+ if (newRefresh.compareTo(storeState.getNextRefreshCheck()) != 0) {
+ updatedState = new State(storeState.incrementRefreshAttempt(), newRefresh);
+ } else {
+ updatedState = new State(storeState, newRefresh);
}
- State updatedState = new State(state, newRefresh);
this.state.put(entry.getKey(), updatedState);
}
}
@@ -181,7 +255,7 @@ void updateNextRefreshTime(Duration refreshInterval, Long defaultMinBackoff) {
* @param defaultMinBackoff min backoff between checks
* @return new Refresh Date
*/
- private Instant getNextRefreshCheck(Instant nextRefreshCheck, Integer attempt, Long interval,
+ private Instant getNextRefreshCheck(Instant nextRefreshCheck, int attempt, Long interval,
Long defaultMinBackoff) {
// The refresh interval is only updated if it is expired.
if (!Instant.now().isAfter(nextRefreshCheck)) {
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlags.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/configuration/WatchedConfigurationSettings.java
similarity index 54%
rename from sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlags.java
rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/configuration/WatchedConfigurationSettings.java
index 2a1380a3d907..20b09087087e 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlags.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/configuration/WatchedConfigurationSettings.java
@@ -1,21 +1,22 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-package com.azure.spring.cloud.appconfiguration.config.implementation.feature;
+package com.azure.spring.cloud.appconfiguration.config.implementation.configuration;
import java.util.List;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.SettingSelector;
-public class FeatureFlags {
+public class WatchedConfigurationSettings {
private SettingSelector settingSelector;
- private List featureFlags;
+ private List configurationSettings;
- public FeatureFlags(SettingSelector settingSelector, List featureFlags) {
+ public WatchedConfigurationSettings(SettingSelector settingSelector,
+ List configurationSettings) {
this.settingSelector = settingSelector;
- this.featureFlags = featureFlags;
+ this.configurationSettings = configurationSettings;
}
/**
@@ -33,17 +34,17 @@ public void setSettingSelector(SettingSelector settingSelector) {
}
/**
- * @return the featureFlags
+ * @return the configurationSettings
*/
- public List getFeatureFlags() {
- return featureFlags;
+ public List getConfigurationSettings() {
+ return configurationSettings;
}
/**
- * @param featureFlags the featureFlags to set
+ * @param configurations the configurations to set
*/
- public void setFeatureFlags(List featureFlags) {
- this.featureFlags = featureFlags;
+ public void setConfigurationSettings(List configurations) {
+ this.configurationSettings = configurations;
}
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlagState.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlagState.java
index ddca37b32140..4df042e96d24 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlagState.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/feature/FeatureFlagState.java
@@ -5,15 +5,17 @@
import java.time.Instant;
import java.util.List;
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
+
public class FeatureFlagState {
- private final List watchKeys;
+ private final List watchKeys;
private final Instant nextRefreshCheck;
private final String originEndpoint;
- public FeatureFlagState(List watchKeys, int refreshInterval, String originEndpoint) {
+ public FeatureFlagState(List watchKeys, int refreshInterval, String originEndpoint) {
this.watchKeys = watchKeys;
nextRefreshCheck = Instant.now().plusSeconds(refreshInterval);
this.originEndpoint = originEndpoint;
@@ -28,7 +30,7 @@ public FeatureFlagState(FeatureFlagState oldState, Instant newRefresh) {
/**
* @return the watchKeys
*/
- public List getWatchKeys() {
+ public List getWatchKeys() {
return watchKeys;
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoring.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoring.java
index f90a3a573f12..02ae16106ec6 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoring.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoring.java
@@ -37,7 +37,7 @@ public final class AppConfigurationStoreMonitoring {
private List triggers = new ArrayList<>();
/**
- * Validation tokens for push notificaiton requests.
+ * Validation tokens for push notification requests.
*/
private PushNotification pushNotification = new PushNotification();
@@ -120,7 +120,7 @@ public void setPushNotification(PushNotification pushNotification) {
@PostConstruct
void validateAndInit() {
if (enabled) {
- Assert.notEmpty(triggers, "Triggers need to be set if refresh is enabled.");
+ // Triggers are not required defaults to use collection monitoring if not set
for (AppConfigurationStoreTrigger trigger : triggers) {
trigger.validateAndInit();
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java
index 36c6e0c2e381..6af627e6afad 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtilTest.java
@@ -2,15 +2,6 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation;
-import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.EMPTY_LABEL;
-import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
@@ -18,6 +9,9 @@
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
@@ -25,6 +19,9 @@
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
@@ -33,10 +30,12 @@
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.data.appconfiguration.models.SettingSelector;
+import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.EMPTY_LABEL;
+import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX;
import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationRefreshUtil.RefreshEventData;
import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlagState;
-import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.AccessToken;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification;
@@ -143,7 +142,8 @@ public void refreshWithoutTimeWatchKeyConfigStoreWatchKeyNoChange(TestInfo testI
when(clientMock.getEndpoint()).thenReturn(endpoint);
FeatureFlagState newState = new FeatureFlagState(
- List.of(new FeatureFlags(new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null)),
+ List.of(new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null)),
Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint);
// Config Store does return a watch key change.
@@ -191,7 +191,8 @@ public void refreshWithoutTimeFeatureFlagNoChange(TestInfo testInfo) {
when(clientMock.getEndpoint()).thenReturn(endpoint);
FeatureFlagState newState = new FeatureFlagState(
- List.of(new FeatureFlags(new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null)),
+ List.of(new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null)),
Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint);
// Config Store doesn't return a watch key change.
@@ -211,7 +212,8 @@ public void refreshWithoutTimeFeatureFlagEtagChanged(TestInfo testInfo) {
endpoint = testInfo.getDisplayName() + ".azconfig.io";
when(clientMock.getEndpoint()).thenReturn(endpoint);
- FeatureFlags featureFlags = new FeatureFlags(new SettingSelector(), watchKeysFeatureFlags);
+ WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(new SettingSelector(),
+ watchKeysFeatureFlags);
FeatureFlagState newState = new FeatureFlagState(List.of(featureFlags),
Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint);
@@ -229,7 +231,10 @@ public void refreshWithoutTimeFeatureFlagEtagChanged(TestInfo testInfo) {
@Test
public void refreshStoresCheckSettingsTestNotEnabled(TestInfo testInfo) {
endpoint = testInfo.getDisplayName() + ".azconfig.io";
- setupFeatureFlagLoad();
+
+ when(connectionManagerMock.getMonitoring()).thenReturn(monitoring);
+ when(connectionManagerMock.getFeatureFlagStore()).thenReturn(featureStore);
+ when(clientFactoryMock.getConnections()).thenReturn(Map.of(endpoint, connectionManagerMock));
State newState = new State(generateWatchKeys(), Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint);
@@ -252,7 +257,10 @@ public void refreshStoresCheckSettingsTestNotEnabled(TestInfo testInfo) {
@Test
public void refreshStoresCheckSettingsTestNotLoaded(TestInfo testInfo) {
endpoint = testInfo.getDisplayName() + ".azconfig.io";
- setupFeatureFlagLoad();
+
+ when(connectionManagerMock.getMonitoring()).thenReturn(monitoring);
+ when(connectionManagerMock.getFeatureFlagStore()).thenReturn(featureStore);
+ when(clientFactoryMock.getConnections()).thenReturn(Map.of(endpoint, connectionManagerMock));
State newState = new State(generateWatchKeys(), Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint);
@@ -343,7 +351,7 @@ public void refreshStoresCheckSettingsTestRefreshTimeNoChange(TestInfo testInfo)
Mockito.any(Context.class));
}
}
-
+
@Test
public void refreshStoresPushRefreshEnabledPrimary(TestInfo testInfo) {
endpoint = testInfo.getDisplayName() + ".azconfig.io";
@@ -380,7 +388,7 @@ public void refreshStoresPushRefreshEnabledPrimary(TestInfo testInfo) {
assertTrue((Boolean) testContext.getData("PushRefresh").get());
}
}
-
+
@Test
public void refreshStoresPushRefreshEnabledSecondary(TestInfo testInfo) {
endpoint = testInfo.getDisplayName() + ".azconfig.io";
@@ -425,7 +433,8 @@ public void refreshStoresCheckSettingsTestTriggerRefresh(TestInfo testInfo) {
when(clientFactoryMock.getConnections()).thenReturn(Map.of(endpoint, connectionManagerMock));
// Refresh Time, trigger refresh
- when(clientFactoryMock.getNextActiveClient(Mockito.eq(endpoint), Mockito.booleanThat(value -> true))).thenReturn(clientOriginMock);
+ when(clientFactoryMock.getNextActiveClient(Mockito.eq(endpoint), Mockito.booleanThat(value -> true)))
+ .thenReturn(clientOriginMock);
ConfigurationSetting refreshKey = new ConfigurationSetting().setKey(KEY_FILTER).setLabel(EMPTY_LABEL)
.setETag("new");
@@ -507,7 +516,8 @@ public void refreshStoresCheckFeatureFlagTestNoChange(TestInfo testInfo) {
when(clientOriginMock.checkWatchKeys(Mockito.any(), Mockito.any(Context.class))).thenReturn(false);
FeatureFlagState newState = new FeatureFlagState(
- List.of(new FeatureFlags(new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null)),
+ List.of(new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null)),
Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint);
// Config Store doesn't return a watch key change.
@@ -533,7 +543,8 @@ public void refreshStoresCheckFeatureFlagTestTriggerRefresh(TestInfo testInfo) {
setupFeatureFlagLoad();
when(clientOriginMock.checkWatchKeys(Mockito.any(), Mockito.any(Context.class))).thenReturn(true);
- FeatureFlags featureFlags = new FeatureFlags(new SettingSelector(), watchKeysFeatureFlags);
+ WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(new SettingSelector(),
+ watchKeysFeatureFlags);
FeatureFlagState newState = new FeatureFlagState(List.of(featureFlags),
Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint);
@@ -568,7 +579,8 @@ private void setupFeatureFlagLoad() {
when(connectionManagerMock.getMonitoring()).thenReturn(monitoring);
when(connectionManagerMock.getFeatureFlagStore()).thenReturn(featureStore);
when(clientFactoryMock.getConnections()).thenReturn(Map.of(endpoint, connectionManagerMock));
- when(clientFactoryMock.getNextActiveClient(Mockito.eq(endpoint), Mockito.booleanThat(value -> true))).thenReturn(clientOriginMock);
+ when(clientFactoryMock.getNextActiveClient(Mockito.eq(endpoint), Mockito.booleanThat(value -> true)))
+ .thenReturn(clientOriginMock);
}
private List generateWatchKeys() {
@@ -589,4 +601,148 @@ private List generateFeatureFlagWatchKeys() {
watchKeys.add(currentWatchKey);
return watchKeys;
}
+
+ @Test
+ public void refreshAllWithWatchedConfigurationSettingsTest(TestInfo testInfo) {
+ // Test that when refreshAll is enabled, watched configuration settings are used instead of watch keys
+ endpoint = testInfo.getDisplayName() + ".azconfig.io";
+
+ when(connectionManagerMock.getMonitoring()).thenReturn(monitoring);
+ when(clientFactoryMock.getConnections()).thenReturn(Map.of(endpoint, connectionManagerMock));
+ when(clientFactoryMock.getNextActiveClient(Mockito.eq(endpoint), Mockito.booleanThat(value -> true)))
+ .thenReturn(clientOriginMock);
+
+ // Set up watched configuration settings state
+ WatchedConfigurationSettings watchedConfigurationSettings = new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null);
+ State state = new State(null, List.of(watchedConfigurationSettings),
+ Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint);
+
+ // Config Store returns a change via watched configuration settings
+ when(clientOriginMock.checkWatchKeys(Mockito.any(SettingSelector.class), Mockito.any(Context.class)))
+ .thenReturn(true);
+
+ try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) {
+ stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true);
+ stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(state);
+ stateHolderMock.when(StateHolder::getCurrentState).thenReturn(currentStateMock);
+
+ RefreshEventData eventData = new AppConfigurationRefreshUtil().refreshStoresCheck(
+ clientFactoryMock, Duration.ofMinutes(10), (long) 60, replicaLookUpMock);
+
+ assertTrue(eventData.getDoRefresh());
+ verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint));
+ // Verify checkWatchKeys is called (watched configuration settings path)
+ verify(clientOriginMock, times(1)).checkWatchKeys(Mockito.any(SettingSelector.class),
+ Mockito.any(Context.class));
+ // Verify getWatchKey is NOT called (traditional watch key path)
+ verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString(),
+ Mockito.any(Context.class));
+ }
+ }
+
+ @Test
+ public void refreshAllWithNullWatchKeysTest(TestInfo testInfo) {
+ // Test that when refreshAll is enabled with null watchKeys, watched configuration settings are still used
+ endpoint = testInfo.getDisplayName() + ".azconfig.io";
+
+ when(connectionManagerMock.getMonitoring()).thenReturn(monitoring);
+ FeatureFlagStore disabledFeatureStore = new FeatureFlagStore();
+ disabledFeatureStore.setEnabled(false);
+ when(connectionManagerMock.getFeatureFlagStore()).thenReturn(disabledFeatureStore);
+ when(clientFactoryMock.getConnections()).thenReturn(Map.of(endpoint, connectionManagerMock));
+ when(clientFactoryMock.getNextActiveClient(Mockito.eq(endpoint), Mockito.booleanThat(value -> true)))
+ .thenReturn(clientOriginMock);
+
+ // Set up state with null watch keys but valid watched configuration settings
+ WatchedConfigurationSettings watchedConfigurationSettings = new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null);
+ State state = new State(null, List.of(watchedConfigurationSettings),
+ Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint);
+
+ when(clientOriginMock.checkWatchKeys(Mockito.any(SettingSelector.class), Mockito.any(Context.class)))
+ .thenReturn(false);
+
+ try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) {
+ stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true);
+ stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(state);
+ stateHolderMock.when(StateHolder::getCurrentState).thenReturn(currentStateMock);
+
+ RefreshEventData eventData = new AppConfigurationRefreshUtil().refreshStoresCheck(
+ clientFactoryMock, Duration.ofMinutes(10), (long) 60, replicaLookUpMock);
+
+ // No change detected, so should not refresh
+ assertFalse(eventData.getDoRefresh());
+ verify(clientOriginMock, times(1)).checkWatchKeys(Mockito.any(SettingSelector.class),
+ Mockito.any(Context.class));
+ }
+ }
+
+ @Test
+ public void watchedConfigurationSettingsNoChangeTest(TestInfo testInfo) {
+ // Test that watched configuration settings correctly detect no change
+ endpoint = testInfo.getDisplayName() + ".azconfig.io";
+
+ when(connectionManagerMock.getMonitoring()).thenReturn(monitoring);
+ FeatureFlagStore disabledFeatureStore = new FeatureFlagStore();
+ disabledFeatureStore.setEnabled(false);
+ when(connectionManagerMock.getFeatureFlagStore()).thenReturn(disabledFeatureStore);
+ when(clientFactoryMock.getConnections()).thenReturn(Map.of(endpoint, connectionManagerMock));
+ when(clientFactoryMock.getNextActiveClient(Mockito.eq(endpoint), Mockito.booleanThat(value -> true)))
+ .thenReturn(clientOriginMock);
+
+ WatchedConfigurationSettings watchedConfigurationSettings = new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL),
+ generateWatchKeys());
+ State state = new State(null, List.of(watchedConfigurationSettings),
+ Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint);
+
+ // Return false indicating no changes detected
+ when(clientOriginMock.checkWatchKeys(Mockito.any(SettingSelector.class), Mockito.any(Context.class)))
+ .thenReturn(false);
+
+ try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) {
+ stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true);
+ stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(state);
+ stateHolderMock.when(StateHolder::getCurrentState).thenReturn(currentStateMock);
+
+ RefreshEventData eventData = new AppConfigurationRefreshUtil().refreshStoresCheck(
+ clientFactoryMock, Duration.ofMinutes(10), (long) 60, replicaLookUpMock);
+
+ assertFalse(eventData.getDoRefresh());
+ verify(currentStateMock, times(1)).updateStateRefresh(Mockito.any(), Mockito.any());
+ }
+ }
+
+ @Test
+ public void watchedConfigurationSettingsWithChangeDetectedTest(TestInfo testInfo) {
+ // Test that watched configuration settings correctly detect changes
+ endpoint = testInfo.getDisplayName() + ".azconfig.io";
+
+ when(connectionManagerMock.getMonitoring()).thenReturn(monitoring);
+ when(clientFactoryMock.getConnections()).thenReturn(Map.of(endpoint, connectionManagerMock));
+ when(clientFactoryMock.getNextActiveClient(Mockito.eq(endpoint), Mockito.booleanThat(value -> true)))
+ .thenReturn(clientOriginMock);
+
+ WatchedConfigurationSettings watchedConfigurationSettings = new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL),
+ generateWatchKeys());
+ State state = new State(null, List.of(watchedConfigurationSettings),
+ Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint);
+
+ // Return true indicating changes detected
+ when(clientOriginMock.checkWatchKeys(Mockito.any(SettingSelector.class), Mockito.any(Context.class)))
+ .thenReturn(true);
+
+ try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) {
+ stateHolderMock.when(() -> StateHolder.getLoadState(endpoint)).thenReturn(true);
+ stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(state);
+ stateHolderMock.when(StateHolder::getCurrentState).thenReturn(currentStateMock);
+
+ RefreshEventData eventData = new AppConfigurationRefreshUtil().refreshStoresCheck(
+ clientFactoryMock, Duration.ofMinutes(10), (long) 60, replicaLookUpMock);
+
+ assertTrue(eventData.getDoRefresh());
+ }
+ }
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java
index b6b48e7288e6..3e18eb2263ec 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientTest.java
@@ -45,6 +45,7 @@
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.data.appconfiguration.models.SnapshotComposition;
import com.azure.identity.CredentialUnavailableException;
+import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings;
import reactor.core.publisher.Mono;
@@ -70,7 +71,7 @@ public class AppConfigurationReplicaClientTest {
@Mock
private Response snapshotResponseMock;
-
+
@Mock
private Context contextMock;
@@ -141,13 +142,16 @@ public void listSettingsTest() {
when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())).thenThrow(exceptionMock);
when(exceptionMock.getResponse()).thenReturn(responseMock);
when(responseMock.getStatusCode()).thenReturn(429);
- assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), contextMock));
+ assertThrows(AppConfigurationStatusException.class,
+ () -> client.listSettings(new SettingSelector(), contextMock));
when(responseMock.getStatusCode()).thenReturn(408);
- assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), contextMock));
+ assertThrows(AppConfigurationStatusException.class,
+ () -> client.listSettings(new SettingSelector(), contextMock));
when(responseMock.getStatusCode()).thenReturn(500);
- assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), contextMock));
+ assertThrows(AppConfigurationStatusException.class,
+ () -> client.listSettings(new SettingSelector(), contextMock));
when(responseMock.getStatusCode()).thenReturn(499);
assertThrows(HttpResponseException.class, () -> client.listSettings(new SettingSelector(), contextMock));
@@ -170,7 +174,8 @@ public void listFeatureFlagsTest() {
when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any()))
.thenReturn(new PagedIterable<>(pagedFlux));
- assertEquals(configurations, client.listFeatureFlags(new SettingSelector(), contextMock).getFeatureFlags());
+ assertEquals(configurations,
+ client.listFeatureFlags(new SettingSelector(), contextMock).getConfigurationSettings());
when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())).thenThrow(exceptionMock);
when(exceptionMock.getResponse()).thenReturn(responseMock);
@@ -196,7 +201,8 @@ public void listSettingsUnknownHostTest() {
when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any()))
.thenThrow(new UncheckedIOException(new UnknownHostException()));
- assertThrows(AppConfigurationStatusException.class, () -> client.listSettings(new SettingSelector(), contextMock));
+ assertThrows(AppConfigurationStatusException.class,
+ () -> client.listSettings(new SettingSelector(), contextMock));
}
@Test
@@ -206,15 +212,17 @@ public void listSettingsNoCredentialTest() {
when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any()))
.thenThrow(new CredentialUnavailableException("No Credential"));
- assertThrows(CredentialUnavailableException.class, () -> client.listSettings(new SettingSelector(), contextMock));
+ assertThrows(CredentialUnavailableException.class,
+ () -> client.listSettings(new SettingSelector(), contextMock));
}
@Test
public void getWatchNoCredentialTest() {
AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock);
- when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any()))
- .thenThrow(new CredentialUnavailableException("No Credential"));
+ when(clientMock.getConfigurationSettingWithResponse(Mockito.any(), Mockito.any(), Mockito.anyBoolean(),
+ Mockito.any()))
+ .thenThrow(new CredentialUnavailableException("No Credential"));
assertThrows(CredentialUnavailableException.class, () -> client.getWatchKey("key", "label", contextMock));
}
@@ -265,20 +273,24 @@ public void listSettingSnapshotTest() {
when(clientMock.listConfigurationSettingsForSnapshot(Mockito.any())).thenThrow(exceptionMock);
when(exceptionMock.getResponse()).thenReturn(responseMock);
when(responseMock.getStatusCode()).thenReturn(429);
- assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", contextMock));
+ assertThrows(AppConfigurationStatusException.class,
+ () -> client.listSettingSnapshot("SnapshotName", contextMock));
when(responseMock.getStatusCode()).thenReturn(408);
- assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", contextMock));
+ assertThrows(AppConfigurationStatusException.class,
+ () -> client.listSettingSnapshot("SnapshotName", contextMock));
when(responseMock.getStatusCode()).thenReturn(500);
- assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", contextMock));
+ assertThrows(AppConfigurationStatusException.class,
+ () -> client.listSettingSnapshot("SnapshotName", contextMock));
when(responseMock.getStatusCode()).thenReturn(499);
assertThrows(HttpResponseException.class, () -> client.listSettingSnapshot("SnapshotName", contextMock));
when(clientMock.getSnapshotWithResponse(Mockito.any(), Mockito.any(), Mockito.any()))
.thenThrow(new UncheckedIOException(new UnknownHostException()));
- assertThrows(AppConfigurationStatusException.class, () -> client.listSettingSnapshot("SnapshotName", contextMock));
+ assertThrows(AppConfigurationStatusException.class,
+ () -> client.listSettingSnapshot("SnapshotName", contextMock));
}
@Test
@@ -340,7 +352,8 @@ public void checkWatchKeysTest() {
when(supplierMock.get()).thenReturn(Mono.just(pagedResponse));
- when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())).thenReturn(new PagedIterable<>(pagedFlux));
+ when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any()))
+ .thenReturn(new PagedIterable<>(pagedFlux));
assertFalse(client.checkWatchKeys(new SettingSelector(), contextMock));
pagedResponse.close();
@@ -349,4 +362,67 @@ public void checkWatchKeysTest() {
}
}
+ @Test
+ public void watchedConfigurationSettingsTest() {
+ AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock);
+
+ ConfigurationSetting setting1 = new ConfigurationSetting().setKey("key1").setLabel("label1");
+ ConfigurationSetting setting2 = new ConfigurationSetting().setKey("key2").setLabel("label2");
+ List configurations = List.of(setting1, setting2);
+
+ PagedFlux pagedFlux = new PagedFlux<>(supplierMock);
+ HttpHeaders headers = new HttpHeaders().add(HttpHeaderName.ETAG, "test-etag-value");
+ PagedResponse pagedResponse = new PagedResponseBase