diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java
index 8fafe59374a6..5d948a8c462e 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java
@@ -2,10 +2,6 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.web.implementation.pullrefresh;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.AppConfigurationWebConstants.ACTUATOR;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH_BUS;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
@@ -13,6 +9,9 @@
import org.springframework.web.context.support.ServletRequestHandledEvent;
import com.azure.spring.cloud.appconfiguration.config.AppConfigurationRefresh;
+import static com.azure.spring.cloud.appconfiguration.config.web.implementation.AppConfigurationWebConstants.ACTUATOR;
+import static com.azure.spring.cloud.appconfiguration.config.web.implementation.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH;
+import static com.azure.spring.cloud.appconfiguration.config.web.implementation.AppConfigurationWebConstants.APPCONFIGURATION_REFRESH_BUS;
/**
* Listens for ServletRequestHandledEvents to check if the configurations need to be updated.
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..f04ef81c78d8 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,17 @@ public class AppConfigurationRefreshUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationRefreshUtil.class);
+ private static final String FEATURE_FLAG_PATTERN = ".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 +78,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 +129,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.
@@ -172,7 +204,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 +226,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 collection monitoring first if configured
+ List watchedSettings = state.getCollectionWatchKeys();
+ if (watchedSettings != null && !watchedSettings.isEmpty()) {
+ refreshWithoutTimeCollectionMonitoring(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 +266,33 @@ private static void refreshWithoutTime(AppConfigurationReplicaClient client, Lis
}
}
+ /**
+ * Checks configuration collection monitoring for etag changes without time validation. This method immediately
+ * checks all collection monitoring selectors for changes regardless of refresh intervals.
+ *
+ * @param client the App Configuration client to use for checking
+ * @param collectionWatchKeys the list of collection monitoring configurations 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 refreshWithoutTimeCollectionMonitoring(AppConfigurationReplicaClient client,
+ List collectionWatchKeys, RefreshEventData eventData, Context context)
+ throws AppConfigurationStatusException {
+ for (WatchedConfigurationSettings collectionMonitoring : collectionWatchKeys) {
+ if (client.checkWatchKeys(collectionMonitoring.getSettingSelector(), context)) {
+ String eventDataInfo = collectionMonitoring.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 collection monitoring: {}", 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 +312,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_PATTERN);
- eventData.setMessage(eventDataInfo);
+ eventData.setMessage(FEATURE_FLAG_PATTERN);
return;
}
@@ -276,15 +341,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_PATTERN);
- eventData.setMessage(eventDataInfo);
+ eventData.setMessage(FEATURE_FLAG_PATTERN);
}
}
@@ -313,9 +376,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 +388,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;
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..511d9d4532fd 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 collection monitoring. 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 CollectionMonitoring containing the retrieved configuration settings and match conditions
+ * @throws HttpResponseException if the request fails
+ */
+ WatchedConfigurationSettings collectionMonitoring(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 CollectionMonitoring 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 5979d37b7f53..e7f3c7e350dd 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,6 +7,7 @@
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;
@@ -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,20 @@ 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 collection monitoring
+ if (monitoring.getTriggers().size() == 0) {
+ // Use collection monitoring for refresh
+ List collectionMonitoringList = getWatchedConfigurationSettings(currentClient);
+ storeState.setState(resource.getEndpoint(), Collections.emptyList(), collectionMonitoringList, 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;
@@ -259,16 +268,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 CollectionMonitoring
* @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);
}
@@ -276,6 +285,40 @@ private List createFeatureFlags(AppConfigurationReplicaClient clie
return featureFlagWatchKeys;
}
+ /**
+ * Creates a list of collection monitoring 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 CollectionMonitoring for configuration settings
+ * @throws Exception creating collection monitoring failed
+ */
+ private List getWatchedConfigurationSettings(AppConfigurationReplicaClient client)
+ throws Exception {
+ List collectionMonitoringList = new ArrayList<>();
+ List selects = resource.getSelects();
+ List profiles = resource.getProfiles().getActive();
+
+ for (AppConfigurationKeyValueSelector selectedKeys : selects) {
+ // Skip snapshots - they don't support collection monitoring
+ if (StringUtils.hasText(selectedKeys.getSnapshotName())) {
+ continue;
+ }
+
+ // Create collection monitoring for each label
+ for (String label : selectedKeys.getLabelFilter(profiles)) {
+ SettingSelector settingSelector = new SettingSelector()
+ .setKeyFilter(selectedKeys.getKeyFilter() + "*")
+ .setLabelFilter(label);
+
+ WatchedConfigurationSettings collectionMonitoring = client.collectionMonitoring(settingSelector, requestContext);
+ collectionMonitoringList.add(collectionMonitoring);
+ }
+ }
+
+ return collectionMonitoringList;
+ }
+
/**
* 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..857ebc2294bd 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,155 @@
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 final 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
+ * Creates a new State with an incremented refresh attempt count.
+ * This method follows the immutable pattern by returning a new instance rather than
+ * modifying the current state. Used when a refresh fails to track attempts for backoff logic.
+ * @return a new State instance with refreshAttempt incremented by 1
*/
- public void incrementRefreshAttempt() {
- this.refreshAttempt += 1;
+ public State withIncrementedRefreshAttempt() {
+ return new State(this, this.nextRefreshCheck, this.refreshAttempt + 1);
}
/**
- * @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..b74a7e3695e6 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.
+ *
+ *
Thread Safety: Uses ConcurrentHashMap for all state maps to ensure thread-safe access
+ * in multi-threaded environments.
+ */
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,12 @@ public void setNextForcedRefresh(Duration refreshPeriod) {
}
/**
+ * 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.
- * @param refreshInterval period between refresh checks.
- * @param defaultMinBackoff min backoff between checks
+ * 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 +233,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.withIncrementedRefreshAttempt(), newRefresh);
+ } else {
+ updatedState = new State(storeState, newRefresh);
}
- State updatedState = new State(state, newRefresh);
this.state.put(entry.getKey(), updatedState);
}
}
@@ -181,7 +256,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 57%
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..782efef549f5 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,21 @@
// 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;
}
/**
@@ -35,15 +35,15 @@ public void setSettingSelector(SettingSelector settingSelector) {
/**
* @return the featureFlags
*/
- 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..ba08e874d2cf 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,7 @@ 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 +190,7 @@ 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 +210,7 @@ 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 +228,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 +254,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);
@@ -507,7 +512,7 @@ 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 +538,7 @@ 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);
@@ -589,4 +594,149 @@ private List generateFeatureFlagWatchKeys() {
watchKeys.add(currentWatchKey);
return watchKeys;
}
+
+ @Test
+ public void refreshAllWithCollectionMonitoringTest(TestInfo testInfo) {
+ // Test that when refreshAll is enabled, collection monitoring is 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 collection monitoring state
+ WatchedConfigurationSettings collectionMonitoring = new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null);
+ State state = new State(null, List.of(collectionMonitoring),
+ Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint);
+
+ // Config Store returns a change via collection monitoring
+ 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 (collection monitoring 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, collection monitoring is 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 collection monitoring
+ WatchedConfigurationSettings collectionMonitoring = new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), null);
+ State state = new State(null, List.of(collectionMonitoring),
+ 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 collectionMonitoringNoChangeTest(TestInfo testInfo) {
+ // Test that collection monitoring correctly detects 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 collectionMonitoring = new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL),
+ generateWatchKeys());
+ State state = new State(null, List.of(collectionMonitoring),
+ 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 collectionMonitoringWithChangeDetectedTest(TestInfo testInfo) {
+ // Test that collection monitoring correctly detects 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 collectionMonitoring = new WatchedConfigurationSettings(
+ new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL),
+ generateWatchKeys());
+ State state = new State(null, List.of(collectionMonitoring),
+ 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..731d6dc0e715 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;
@@ -170,7 +171,7 @@ 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);
@@ -349,4 +350,67 @@ public void checkWatchKeysTest() {
}
}
+ @Test
+ public void collectionMonitoringTest() {
+ 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