From 051cc44eabca8775837fa4450a605dc2d855f797 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 5 Nov 2025 14:17:18 -0800 Subject: [PATCH 01/13] start collection monitoring --- .../AppConfigurationRefreshUtil.java | 6 ++-- .../AppConfigurationReplicaClient.java | 29 +++++++++++++-- ...ppConfigurationSnapshotPropertySource.java | 4 +-- .../AzureAppConfigDataLoader.java | 12 +++---- .../implementation/FeatureFlagClient.java | 14 ++++---- .../config/implementation/StateHolder.java | 4 +-- .../CollectionMonitoring.java} | 20 +++++------ .../feature/FeatureFlagState.java | 8 +++-- .../AppConfigurationStoreMonitoring.java | 18 ++++++++++ .../AppConfigurationRefreshUtilTest.java | 12 +++---- .../AppConfigurationReplicaClientTest.java | 2 +- .../implementation/FeatureFlagClientTest.java | 36 +++++++++---------- 12 files changed, 104 insertions(+), 61 deletions(-) rename sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/{feature/FeatureFlags.java => configuration/CollectionMonitoring.java} (60%) 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..a0192cf9681f 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.CollectionMonitoring; 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; @@ -245,7 +245,7 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl if (date.isAfter(state.getNextRefreshCheck())) { replicaLookUp.updateAutoFailoverEndpoints(); - for (FeatureFlags featureFlags : state.getWatchKeys()) { + for (CollectionMonitoring featureFlags : state.getWatchKeys()) { if (client.checkWatchKeys(featureFlags.getSettingSelector(), context)) { String eventDataInfo = ".appconfig.featureflag/*"; @@ -276,7 +276,7 @@ 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 (CollectionMonitoring featureFlags : watchKeys.getWatchKeys()) { if (client.checkWatchKeys(featureFlags.getSettingSelector(), context)) { String eventDataInfo = ".appconfig.featureflag/*"; 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..a4f4e55594f7 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.CollectionMonitoring; import io.netty.handler.codec.http.HttpResponseStatus; @@ -150,6 +150,29 @@ List listSettings(SettingSelector settingSelector, Context } } + CollectionMonitoring 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 featureFlag : pagedResponse.getValue()) { + configurationSettings.add(NormalizeNull.normalizeNullLabel(featureFlag)); + } + }); + + // Needs to happen after or we don't know if the request succeeded or failed. + this.failedAttempts = 0; + settingSelector.setMatchConditions(checks); + return new CollectionMonitoring(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. * @@ -158,7 +181,7 @@ List listSettings(SettingSelector settingSelector, Context * @return FeatureFlags containing the retrieved feature flags and match conditions * @throws HttpResponseException if the request fails */ - FeatureFlags listFeatureFlags(SettingSelector settingSelector, Context context) + CollectionMonitoring listFeatureFlags(SettingSelector settingSelector, Context context) throws HttpResponseException { List configurationSettings = new ArrayList<>(); List checks = new ArrayList<>(); @@ -175,7 +198,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 CollectionMonitoring(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..f31aa638a87b 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.CollectionMonitoring; /** * 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); + CollectionMonitoring featureFlags = new CollectionMonitoring(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..b642d9c9446e 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 @@ -22,7 +22,7 @@ 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.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; 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; @@ -152,7 +152,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(); @@ -259,16 +259,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); } 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..5e0e309635e2 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.CollectionMonitoring; 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,18 @@ List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter).setLabelFilter(label); context.addData("FeatureFlagTracing", tracing); - FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, context); + CollectionMonitoring features = replicaClient.listFeatureFlags(settingSelector, context); loadedFeatureFlags.addAll(proccessFeatureFlags(features, replicaClient.getOriginClient())); } return loadedFeatureFlags; } - List proccessFeatureFlags(FeatureFlags features, String endpoint) { - List loadedFeatureFlags = new ArrayList<>(); + List proccessFeatureFlags(CollectionMonitoring features, String endpoint) { + List loadedFeatureFlags = new ArrayList<>(); loadedFeatureFlags.add(features); // Reading In Features - for (ConfigurationSetting setting : features.getFeatureFlags()) { + for (ConfigurationSetting setting : features.getConfigurations()) { if (setting instanceof FeatureFlagConfigurationSetting && FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) { FeatureFlagConfigurationSetting featureFlag = (FeatureFlagConfigurationSetting) setting; 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..b7bb90f4320a 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,8 +11,8 @@ import java.util.concurrent.ConcurrentHashMap; import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlagState; -import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags; final class StateHolder { @@ -84,7 +84,7 @@ void setState(String originEndpoint, List watchKeys, Durat * @param watchKeys list of configuration 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)); 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/CollectionMonitoring.java similarity index 60% 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/CollectionMonitoring.java index 2a1380a3d907..aa70f07f6129 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/CollectionMonitoring.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 CollectionMonitoring { private SettingSelector settingSelector; - private List featureFlags; + private List configurations; - public FeatureFlags(SettingSelector settingSelector, List featureFlags) { + public CollectionMonitoring(SettingSelector settingSelector, List configurations) { this.settingSelector = settingSelector; - this.featureFlags = featureFlags; + this.configurations = configurations; } /** @@ -35,15 +35,15 @@ public void setSettingSelector(SettingSelector settingSelector) { /** * @return the featureFlags */ - public List getFeatureFlags() { - return featureFlags; + public List getConfigurations() { + return configurations; } /** - * @param featureFlags the featureFlags to set + * @param configurations the configurations to set */ - public void setFeatureFlags(List featureFlags) { - this.featureFlags = featureFlags; + public void setConfigurations(List configurations) { + this.configurations = 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..34124e900d36 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.CollectionMonitoring; + 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..daedc6fd8a0b 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 @@ -36,6 +36,8 @@ public final class AppConfigurationStoreMonitoring { */ private List triggers = new ArrayList<>(); + private Boolean refreshAll = false; + /** * Validation tokens for push notificaiton requests. */ @@ -114,12 +116,28 @@ public void setPushNotification(PushNotification pushNotification) { this.pushNotification = pushNotification; } + /** + * @return the refreshAll + */ + public Boolean getRefreshAll() { + return refreshAll; + } + + /** + * @param refreshAll the refreshAll to set + */ + public void setRefreshAll(Boolean refreshAll) { + this.refreshAll = refreshAll; + } + /** * Validates refreshIntervals are at least 1 second, and if enabled triggers are valid. */ @PostConstruct void validateAndInit() { if (enabled) { + + Assert.notEmpty(triggers, "Triggers need to be set if refresh is enabled."); 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..a9498dd9f4e9 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 @@ -35,8 +35,8 @@ import com.azure.data.appconfiguration.models.SettingSelector; 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.CollectionMonitoring; 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 +143,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 CollectionMonitoring(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,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 CollectionMonitoring(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 +211,7 @@ public void refreshWithoutTimeFeatureFlagEtagChanged(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - FeatureFlags featureFlags = new FeatureFlags(new SettingSelector(), watchKeysFeatureFlags); + CollectionMonitoring featureFlags = new CollectionMonitoring(new SettingSelector(), watchKeysFeatureFlags); FeatureFlagState newState = new FeatureFlagState(List.of(featureFlags), Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); @@ -507,7 +507,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 CollectionMonitoring(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 +533,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); + CollectionMonitoring featureFlags = new CollectionMonitoring(new SettingSelector(), watchKeysFeatureFlags); FeatureFlagState newState = new FeatureFlagState(List.of(featureFlags), Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint); 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..12d31caf1b42 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 @@ -170,7 +170,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).getConfigurations()); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())).thenThrow(exceptionMock); when(exceptionMock.getResponse()).thenReturn(responseMock); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java index f2ad1eca8024..1198b93c37b2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java @@ -36,7 +36,7 @@ import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagFilter; -import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags; +import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; 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.Variant; @@ -80,14 +80,14 @@ public void cleanup() throws Exception { @Test public void loadFeatureFlagsTestNoFeatureFlags() { List settings = List.of(new ConfigurationSetting().setKey("FakeKey")); - FeatureFlags featureFlags = new FeatureFlags(null, settings); + CollectionMonitoring featureFlags = new CollectionMonitoring(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); - assertEquals("FakeKey", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); + assertEquals("FakeKey", featureFlagsList.get(0).getConfigurations().get(0).getKey()); assertEquals(0, featureFlagClient.getFeatureFlags().size()); } @@ -95,15 +95,15 @@ public void loadFeatureFlagsTestNoFeatureFlags() { public void loadFeatureFlagsTestFeatureFlags() { List settings = List.of(new FeatureFlagConfigurationSetting("Alpha", false), new FeatureFlagConfigurationSetting("Beta", true)); - FeatureFlags featureFlags = new FeatureFlags(null, settings); + CollectionMonitoring featureFlags = new CollectionMonitoring(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); - assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); - assertEquals(".appconfig.featureflag/Beta", featureFlagsList.get(0).getFeatureFlags().get(1).getKey()); + assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getConfigurations().get(0).getKey()); + assertEquals(".appconfig.featureflag/Beta", featureFlagsList.get(0).getConfigurations().get(1).getKey()); assertEquals(2, featureFlagClient.getFeatureFlags().size()); } @@ -111,27 +111,27 @@ public void loadFeatureFlagsTestFeatureFlags() { public void loadFeatureFlagsTestMultipleLoads() { List settings = List.of(new FeatureFlagConfigurationSetting("Alpha", false), new FeatureFlagConfigurationSetting("Beta", true)); - FeatureFlags featureFlags = new FeatureFlags(null, settings); + CollectionMonitoring featureFlags = new CollectionMonitoring(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); - assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); - assertEquals(".appconfig.featureflag/Beta", featureFlagsList.get(0).getFeatureFlags().get(1).getKey()); + assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getConfigurations().get(0).getKey()); + assertEquals(".appconfig.featureflag/Beta", featureFlagsList.get(0).getConfigurations().get(1).getKey()); assertEquals(2, featureFlagClient.getFeatureFlags().size()); List settings2 = List.of(new FeatureFlagConfigurationSetting("Alpha", true), new FeatureFlagConfigurationSetting("Gamma", false)); - featureFlags = new FeatureFlags(null, settings2); + featureFlags = new CollectionMonitoring(null, settings2); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); - assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); - assertEquals(".appconfig.featureflag/Gamma", featureFlagsList.get(0).getFeatureFlags().get(1).getKey()); + assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getConfigurations().get(0).getKey()); + assertEquals(".appconfig.featureflag/Gamma", featureFlagsList.get(0).getConfigurations().get(1).getKey()); assertEquals(3, featureFlagClient.getFeatureFlags().size()); List features = featureFlagClient.getFeatureFlags(); assertTrue(features.get(0).isEnabled()); @@ -170,14 +170,14 @@ public void loadFeatureFlagsTestTargetingFilter() { targetingFilter.addParameter("Audience", parameters); targetingFlag.addClientFilter(targetingFilter); List settings = List.of(targetingFlag); - FeatureFlags featureFlags = new FeatureFlags(null, settings); + CollectionMonitoring featureFlags = new CollectionMonitoring(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); - assertEquals(".appconfig.featureflag/TargetingTest", featureFlagsList.get(0).getFeatureFlags().get(0).getKey()); + assertEquals(".appconfig.featureflag/TargetingTest", featureFlagsList.get(0).getConfigurations().get(0).getKey()); assertEquals(1, featureFlagClient.getFeatureFlags().size()); } From 67108e90ecd6373f0301a93e802d09484767f282 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 18 Dec 2025 16:40:03 -0500 Subject: [PATCH 02/13] Updating collection monitoring --- .../AppConfigurationEventListener.java | 7 +-- .../AppConfigurationRefreshUtil.java | 39 ++++++++++++- .../AppConfigurationReplicaClient.java | 13 ++++- .../AzureAppConfigDataLoader.java | 56 ++++++++++++++++--- .../config/implementation/State.java | 21 +++++++ .../config/implementation/StateHolder.java | 10 ++++ .../AppConfigurationStoreMonitoring.java | 7 ++- 7 files changed, 134 insertions(+), 19 deletions(-) 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 a0192cf9681f..193d14be3aaa 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 @@ -2,8 +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.PUSH_REFRESH; - import java.time.Duration; import java.time.Instant; import java.util.List; @@ -16,6 +14,7 @@ import com.azure.core.exception.HttpResponseException; import com.azure.core.util.Context; import com.azure.data.appconfiguration.models.ConfigurationSetting; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlagState; @@ -194,7 +193,14 @@ 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 + if (state.getCollectionWatchKeys() != null && !state.getCollectionWatchKeys().isEmpty()) { + refreshWithoutTimeCollectionMonitoring(client, state.getCollectionWatchKeys(), eventData, context); + } else { + // Fall back to traditional watch key monitoring + refreshWithoutTime(client, state.getWatchKeys(), eventData, context); + } StateHolder.getCurrentState().updateStateRefresh(state, refreshInterval); } @@ -226,6 +232,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 (CollectionMonitoring collectionMonitoring : collectionWatchKeys) { + if (client.checkWatchKeys(collectionMonitoring.getSettingSelector(), context)) { + String eventDataInfo = collectionMonitoring.getSettingSelector().getKeyFilter(); + + // Only one refresh Event needs to be call 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. 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 a4f4e55594f7..93aadc0dadba 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 @@ -150,6 +150,15 @@ 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 + */ CollectionMonitoring collectionMonitoring(SettingSelector settingSelector, Context context) { List configurationSettings = new ArrayList<>(); List checks = new ArrayList<>(); @@ -157,8 +166,8 @@ CollectionMonitoring collectionMonitoring(SettingSelector settingSelector, Conte client.listConfigurationSettings(settingSelector, context).streamByPage().forEach(pagedResponse -> { checks.add( new MatchConditions().setIfNoneMatch(pagedResponse.getHeaders().getValue(HttpHeaderName.ETAG))); - for (ConfigurationSetting featureFlag : pagedResponse.getValue()) { - configurationSettings.add(NormalizeNull.normalizeNullLabel(featureFlag)); + for (ConfigurationSetting setting : pagedResponse.getValue()) { + configurationSettings.add(NormalizeNull.normalizeNullLabel(setting)); } }); 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 b642d9c9446e..2f498fda104b 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 @@ -22,6 +22,7 @@ import com.azure.core.util.Context; import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.data.appconfiguration.models.SettingSelector; import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring; @@ -161,13 +162,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 (Boolean.TRUE.equals(monitoring.getRefreshAll())) { + // Use collection monitoring for refresh + List collectionMonitoringList = createCollectionMonitoring(currentClient); + storeState.setState(resource.getEndpoint(), null, 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; @@ -276,6 +284,40 @@ private List createFeatureFlags(AppConfigurationReplicaCli 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 createCollectionMonitoring(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); + + CollectionMonitoring 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/State.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/State.java index 1d237001e78e..e8e997eb72f4 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,11 +6,14 @@ import java.util.List; import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; class State { private final List watchKeys; + private final List collectionWatchKeys; + private final Instant nextRefreshCheck; private final String originEndpoint; @@ -21,6 +24,16 @@ class State { State(List watchKeys, int refreshInterval, String originEndpoint) { this.watchKeys = watchKeys; + this.collectionWatchKeys = null; + this.refreshInterval = refreshInterval; + nextRefreshCheck = Instant.now().plusSeconds(refreshInterval); + this.originEndpoint = originEndpoint; + this.refreshAttempt = 1; + } + + State(List watchKeys, List collectionWatchKeys, int refreshInterval, String originEndpoint) { + this.watchKeys = watchKeys; + this.collectionWatchKeys = collectionWatchKeys; this.refreshInterval = refreshInterval; nextRefreshCheck = Instant.now().plusSeconds(refreshInterval); this.originEndpoint = originEndpoint; @@ -29,6 +42,7 @@ class State { State(State oldState, Instant newRefresh) { this.watchKeys = oldState.getWatchKeys(); + this.collectionWatchKeys = oldState.getCollectionWatchKeys(); this.refreshInterval = oldState.getRefreshInterval(); this.nextRefreshCheck = newRefresh; this.originEndpoint = oldState.getOriginEndpoint(); @@ -42,6 +56,13 @@ public List getWatchKeys() { return watchKeys; } + /** + * @return the collectionWatchKeys + */ + public List getCollectionWatchKeys() { + return collectionWatchKeys; + } + /** * @return the nextRefreshCheck */ 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 b7bb90f4320a..ccb7a8ed98bf 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 @@ -79,6 +79,16 @@ void setState(String originEndpoint, List watchKeys, Durat state.put(originEndpoint, new State(watchKeys, Math.toIntExact(duration.getSeconds()), originEndpoint)); } + /** + * @param originEndpoint the stores origin endpoint + * @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 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)); + } + /** * @param originEndpoint the stores origin endpoint * @param watchKeys list of configuration watch keys that can trigger a refresh event 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 daedc6fd8a0b..c816ece3bb93 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 @@ -136,9 +136,10 @@ public void setRefreshAll(Boolean refreshAll) { @PostConstruct void validateAndInit() { if (enabled) { - - - Assert.notEmpty(triggers, "Triggers need to be set if refresh is enabled."); + // Triggers are required unless refreshAll is enabled (which uses collection monitoring instead) + if (!Boolean.TRUE.equals(refreshAll)) { + Assert.notEmpty(triggers, "Triggers need to be set if refresh is enabled and refreshAll is not enabled."); + } for (AppConfigurationStoreTrigger trigger : triggers) { trigger.validateAndInit(); } From 04880e4014008f8f654f365f4d3025b6b59c207c Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 18 Dec 2025 16:48:19 -0500 Subject: [PATCH 03/13] Updating State --- .../config/implementation/State.java | 46 ++++++++++--------- .../config/implementation/StateHolder.java | 13 ++++-- 2 files changed, 32 insertions(+), 27 deletions(-) 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 e8e997eb72f4..fafed10f7a09 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 @@ -18,35 +18,36 @@ class State { private final String originEndpoint; - private Integer refreshAttempt; + private final int refreshAttempt; private final int refreshInterval; State(List watchKeys, int refreshInterval, String originEndpoint) { - this.watchKeys = watchKeys; - this.collectionWatchKeys = null; - this.refreshInterval = refreshInterval; - nextRefreshCheck = Instant.now().plusSeconds(refreshInterval); - this.originEndpoint = originEndpoint; - this.refreshAttempt = 1; + this(watchKeys, null, refreshInterval, originEndpoint); } State(List watchKeys, List collectionWatchKeys, int refreshInterval, String originEndpoint) { + this(watchKeys, collectionWatchKeys, refreshInterval, originEndpoint, Instant.now().plusSeconds(refreshInterval), 1); + } + + State(State oldState, Instant newRefresh) { + this(oldState, newRefresh, oldState.getRefreshAttempt()); + } + + State(State oldState, Instant newRefresh, int refreshAttempt) { + this(oldState.getWatchKeys(), oldState.getCollectionWatchKeys(), oldState.getRefreshInterval(), + oldState.getOriginEndpoint(), newRefresh, refreshAttempt); + } + + // Primary constructor + private State(List watchKeys, List collectionWatchKeys, + int refreshInterval, String originEndpoint, Instant nextRefreshCheck, int refreshAttempt) { this.watchKeys = watchKeys; this.collectionWatchKeys = collectionWatchKeys; this.refreshInterval = refreshInterval; - nextRefreshCheck = Instant.now().plusSeconds(refreshInterval); + this.nextRefreshCheck = nextRefreshCheck; this.originEndpoint = originEndpoint; - this.refreshAttempt = 1; - } - - State(State oldState, Instant newRefresh) { - this.watchKeys = oldState.getWatchKeys(); - this.collectionWatchKeys = oldState.getCollectionWatchKeys(); - this.refreshInterval = oldState.getRefreshInterval(); - this.nextRefreshCheck = newRefresh; - this.originEndpoint = oldState.getOriginEndpoint(); - this.refreshAttempt = oldState.getRefreshAttempt(); + this.refreshAttempt = refreshAttempt; } /** @@ -80,15 +81,16 @@ public String getOriginEndpoint() { /** * @return the refreshAttempt */ - 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 + * @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); } /** 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 ccb7a8ed98bf..21573649031c 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 @@ -170,14 +170,17 @@ 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); + State currentState = entry.getValue(); + Instant newRefresh = getNextRefreshCheck(currentState.getNextRefreshCheck(), + currentState.getRefreshAttempt(), (long) currentState.getRefreshInterval(), defaultMinBackoff); + State updatedState; if (newRefresh.compareTo(entry.getValue().getNextRefreshCheck()) != 0) { - state.incrementRefreshAttempt(); + updatedState = currentState.withIncrementedRefreshAttempt(); + updatedState = new State(updatedState, newRefresh); + } else { + updatedState = new State(currentState, newRefresh); } - State updatedState = new State(state, newRefresh); this.state.put(entry.getKey(), updatedState); } } From cce68816d777b3c40dd5da86b6a6d3d909968621 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 18 Dec 2025 16:54:56 -0500 Subject: [PATCH 04/13] Updated docs --- .../config/implementation/State.java | 76 ++++++++++-- .../config/implementation/StateHolder.java | 110 ++++++++++++++---- 2 files changed, 154 insertions(+), 32 deletions(-) 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 fafed10f7a09..3cb4bf90af00 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 @@ -8,38 +8,90 @@ import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; +/** + * 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; + /** 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, 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(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 + /** + * 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; @@ -51,42 +103,49 @@ private State(List watchKeys, List c } /** - * @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 collectionWatchKeys + * 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; } /** - * @return the nextRefreshCheck + * 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 int getRefreshAttempt() { return refreshAttempt; } /** - * Creates a new State with an incremented refresh attempt count + * 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 State withIncrementedRefreshAttempt() { @@ -94,7 +153,8 @@ public State withIncrementedRefreshAttempt() { } /** - * @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 21573649031c..14edc1e95516 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 @@ -14,85 +14,128 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlagState; +/** + * 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 collectionWatchKeys list of collection monitoring configurations that can trigger a refresh event - * @param duration refresh duration. + * @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)); } /** - * @param originEndpoint the stores origin endpoint - * @param watchKeys list of configuration watch keys that can trigger a refresh event - * @param duration refresh duration. + * 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, Duration duration) { @@ -100,16 +143,31 @@ void setStateFeatureFlag(String originEndpoint, List watch 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); @@ -121,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); @@ -136,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) { @@ -153,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) { @@ -170,16 +233,15 @@ void updateNextRefreshTime(Duration refreshInterval, Long defaultMinBackoff) { } for (Entry entry : state.entrySet()) { - State currentState = entry.getValue(); - Instant newRefresh = getNextRefreshCheck(currentState.getNextRefreshCheck(), - currentState.getRefreshAttempt(), (long) currentState.getRefreshInterval(), defaultMinBackoff); + State storeState = entry.getValue(); + Instant newRefresh = getNextRefreshCheck(storeState.getNextRefreshCheck(), + storeState.getRefreshAttempt(), (long) storeState.getRefreshInterval(), defaultMinBackoff); State updatedState; - if (newRefresh.compareTo(entry.getValue().getNextRefreshCheck()) != 0) { - updatedState = currentState.withIncrementedRefreshAttempt(); - updatedState = new State(updatedState, newRefresh); + if (newRefresh.compareTo(storeState.getNextRefreshCheck()) != 0) { + updatedState = new State(storeState.withIncrementedRefreshAttempt(), newRefresh); } else { - updatedState = new State(currentState, newRefresh); + updatedState = new State(storeState, newRefresh); } this.state.put(entry.getKey(), updatedState); } @@ -194,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)) { From 16023185558f044bb2a2e80801f22fbe814fcf61 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 7 Jan 2026 16:52:12 -0800 Subject: [PATCH 05/13] cleaning up refresh --- .../AppConfigurationRefreshUtil.java | 148 +++++++++++------- .../AzureAppConfigDataLoader.java | 2 +- 2 files changed, 89 insertions(+), 61 deletions(-) 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 193d14be3aaa..bfce8b93ebf4 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 @@ -29,6 +29,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. * @@ -66,66 +77,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); - 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); } } @@ -137,6 +128,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. @@ -171,7 +203,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(); } @@ -249,9 +281,9 @@ private static void refreshWithoutTimeCollectionMonitoring(AppConfigurationRepli if (client.checkWatchKeys(collectionMonitoring.getSettingSelector(), context)) { String eventDataInfo = collectionMonitoring.getSettingSelector().getKeyFilter(); - // 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 collection monitoring: " + eventDataInfo); + LOGGER.info("Configuration Refresh Event triggered by collection monitoring: {}", eventDataInfo); eventData.setMessage(eventDataInfo); return; @@ -280,13 +312,11 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl for (CollectionMonitoring 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; } @@ -311,13 +341,11 @@ private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient for (CollectionMonitoring 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); } } @@ -346,9 +374,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); } } @@ -358,7 +386,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/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java index 2f498fda104b..2394eb10f626 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 @@ -128,7 +128,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. From b65c1d82f395d79b37ee3f1c7ea29246e31edcaf Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Thu, 8 Jan 2026 10:41:02 -0800 Subject: [PATCH 06/13] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../properties/AppConfigurationStoreMonitoring.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 c816ece3bb93..d01b219173ac 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 @@ -39,7 +39,7 @@ public final class AppConfigurationStoreMonitoring { private Boolean refreshAll = false; /** - * Validation tokens for push notificaiton requests. + * Validation tokens for push notification requests. */ private PushNotification pushNotification = new PushNotification(); @@ -139,9 +139,9 @@ void validateAndInit() { // Triggers are required unless refreshAll is enabled (which uses collection monitoring instead) if (!Boolean.TRUE.equals(refreshAll)) { Assert.notEmpty(triggers, "Triggers need to be set if refresh is enabled and refreshAll is not enabled."); - } - for (AppConfigurationStoreTrigger trigger : triggers) { - trigger.validateAndInit(); + for (AppConfigurationStoreTrigger trigger : triggers) { + trigger.validateAndInit(); + } } } Assert.isTrue(refreshInterval.getSeconds() >= 1, "Minimum refresh interval time is 1 Second."); From 9ebdb417cab47a64ebe1d55f6b1d28142f6fa26a Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 8 Jan 2026 11:15:40 -0800 Subject: [PATCH 07/13] fixing tests --- .../AppConfigurationRefreshUtilTest.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) 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 a9498dd9f4e9..31ee64795999 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,6 +30,8 @@ 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.CollectionMonitoring; @@ -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); From b0b200d2742fb8ecee29a4ece7d46f31997f9617 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 8 Jan 2026 12:01:16 -0800 Subject: [PATCH 08/13] New tests --- .../AppConfigurationRefreshUtilTest.java | 155 ++++++++++++++++++ .../AppConfigurationReplicaClientTest.java | 81 ++++++++- .../AppConfigurationStoreMonitoringTest.java | 64 +++++++- 3 files changed, 288 insertions(+), 12 deletions(-) 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 31ee64795999..81f1b048e3c8 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 @@ -594,4 +594,159 @@ 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"; + + monitoring.setRefreshAll(true); + 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 collection monitoring state + CollectionMonitoring collectionMonitoring = new CollectionMonitoring( + 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"; + + monitoring.setRefreshAll(true); + 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 + CollectionMonitoring collectionMonitoring = new CollectionMonitoring( + 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"; + + monitoring.setRefreshAll(true); + 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); + + CollectionMonitoring collectionMonitoring = new CollectionMonitoring( + 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"; + + monitoring.setRefreshAll(true); + 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); + + CollectionMonitoring collectionMonitoring = new CollectionMonitoring( + 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 12d31caf1b42..4ef1855d75b1 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 @@ -2,15 +2,6 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import java.io.IOException; import java.io.UncheckedIOException; import java.net.UnknownHostException; @@ -20,10 +11,18 @@ import java.util.function.Supplier; 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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.Mockito; +import static org.mockito.Mockito.reset; +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; @@ -45,6 +44,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.CollectionMonitoring; import reactor.core.publisher.Mono; @@ -349,4 +349,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( + null, 200, headers, configurations, null, null); + + when(supplierMock.get()).thenReturn(Mono.just(pagedResponse)); + when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) + .thenReturn(new PagedIterable<>(pagedFlux)); + + SettingSelector selector = new SettingSelector().setKeyFilter("*"); + CollectionMonitoring result = client.collectionMonitoring(selector, contextMock); + + assertEquals(2, result.getConfigurations().size()); + assertEquals("key1", result.getConfigurations().get(0).getKey()); + assertEquals("key2", result.getConfigurations().get(1).getKey()); + assertEquals(1, result.getSettingSelector().getMatchConditions().size()); + assertEquals("test-etag-value", result.getSettingSelector().getMatchConditions().get(0).getIfNoneMatch()); + assertEquals(0, client.getFailedAttempts()); + } + + @Test + public void collectionMonitoringErrorTest() { + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + + when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())).thenThrow(exceptionMock); + when(exceptionMock.getResponse()).thenReturn(responseMock); + when(responseMock.getStatusCode()).thenReturn(429); + + assertThrows(AppConfigurationStatusException.class, + () -> client.collectionMonitoring(new SettingSelector(), contextMock)); + + when(responseMock.getStatusCode()).thenReturn(408); + assertThrows(AppConfigurationStatusException.class, + () -> client.collectionMonitoring(new SettingSelector(), contextMock)); + + when(responseMock.getStatusCode()).thenReturn(500); + assertThrows(AppConfigurationStatusException.class, + () -> client.collectionMonitoring(new SettingSelector(), contextMock)); + + when(responseMock.getStatusCode()).thenReturn(499); + assertThrows(HttpResponseException.class, + () -> client.collectionMonitoring(new SettingSelector(), contextMock)); + } + + @Test + public void collectionMonitoringUncheckedIOExceptionTest() { + AppConfigurationReplicaClient client = new AppConfigurationReplicaClient(endpoint, endpoint, clientMock); + + when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())) + .thenThrow(new UncheckedIOException(new IOException("Network error"))); + + assertThrows(AppConfigurationStatusException.class, + () -> client.collectionMonitoring(new SettingSelector(), contextMock)); + } + } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoringTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoringTest.java index ab4f2e6c0656..060e11ec0f53 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoringTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoringTest.java @@ -2,13 +2,12 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation.properties; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - import java.time.Duration; import java.util.ArrayList; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; public class AppConfigurationStoreMonitoringTest { @@ -51,4 +50,63 @@ public void validateAndInitTest() { monitoring.validateAndInit(); } + @Test + public void refreshAllEnabledWithoutTriggersTest() { + // When refreshAll is enabled, triggers are not required + AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + monitoring.setEnabled(true); + monitoring.setRefreshAll(true); + + // Should not throw an exception even with no triggers + monitoring.validateAndInit(); + + // Verify refresh interval validation still applies + monitoring.setRefreshInterval(Duration.ofSeconds(0)); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); + assertEquals("Minimum refresh interval time is 1 Second.", e.getMessage()); + } + + @Test + public void refreshAllDisabledRequiresTriggersTest() { + // When refreshAll is disabled or null, triggers are required + AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + monitoring.setEnabled(true); + monitoring.setRefreshAll(false); + + // Should throw an exception with no triggers + assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); + + // Setting refreshAll to null should also require triggers + monitoring.setRefreshAll(null); + assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); + } + + @Test + public void refreshAllWithTriggersTest() { + // Even when refreshAll is enabled, having triggers should still be valid + AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + monitoring.setEnabled(true); + monitoring.setRefreshAll(true); + + List triggers = new ArrayList<>(); + AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); + trigger.setKey("sentinel"); + triggers.add(trigger); + monitoring.setTriggers(triggers); + + // Should not throw an exception + monitoring.validateAndInit(); + } + + @Test + public void monitoringDisabledWithRefreshAllTest() { + // When monitoring is disabled, refreshAll setting should not matter + AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + monitoring.setEnabled(false); + monitoring.setRefreshAll(true); + + // Should not throw an exception even with no triggers + monitoring.validateAndInit(); + } + } From e91c40774c4a94c7ae2079039ba8f88531179dd7 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 8 Jan 2026 12:19:45 -0800 Subject: [PATCH 09/13] More new tests --- .../AppConfigurationRefreshUtilTest.java | 6 - .../AzureAppConfigDataLoaderTest.java | 269 ++++++++++++++++++ 2 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java 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 81f1b048e3c8..8c317029b9b7 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 @@ -602,9 +602,6 @@ public void refreshAllWithCollectionMonitoringTest(TestInfo testInfo) { monitoring.setRefreshAll(true); 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); @@ -720,9 +717,6 @@ public void collectionMonitoringWithChangeDetectedTest(TestInfo testInfo) { monitoring.setRefreshAll(true); 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); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java new file mode 100644 index 000000000000..ea3b1ddcd0c2 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.appconfiguration.config.implementation; + +import java.time.Duration; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import static org.mockito.ArgumentMatchers.any; +import org.mockito.Mock; +import org.mockito.Mockito; +import static org.mockito.Mockito.lenient; +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; +import org.springframework.boot.context.config.Profiles; + +import com.azure.core.util.Context; +import com.azure.data.appconfiguration.models.SettingSelector; +import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; +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.AppConfigurationStoreTrigger; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore; + +public class AzureAppConfigDataLoaderTest { + + @Mock + private AppConfigurationReplicaClient clientMock; + + @Mock + private CollectionMonitoring collectionMonitoringMock; + + private AzureAppConfigDataResource resource; + + private ConfigStore configStore; + + private MockitoSession session; + + private static final String ENDPOINT = "https://test.azconfig.io"; + + private static final String KEY_FILTER = "/application/*"; + + private static final String LABEL_FILTER = "prod"; + + @BeforeEach + public void setup() { + session = Mockito.mockitoSession().initMocks(this).strictness(Strictness.STRICT_STUBS).startMocking(); + MockitoAnnotations.openMocks(this); + + configStore = new ConfigStore(); + configStore.setEndpoint(ENDPOINT); + configStore.setEnabled(true); + + // Setup feature flags + FeatureFlagStore featureFlagStore = new FeatureFlagStore(); + featureFlagStore.setEnabled(false); + configStore.setFeatureFlags(featureFlagStore); + + // Setup basic resource + Profiles profiles = Mockito.mock(Profiles.class); + lenient().when(profiles.getActive()).thenReturn(List.of(LABEL_FILTER)); + + resource = new AzureAppConfigDataResource(true, configStore, profiles, false, Duration.ofMinutes(1)); + } + + @AfterEach + public void cleanup() throws Exception { + MockitoAnnotations.openMocks(this).close(); + session.finishMocking(); + } + + @Test + public void createCollectionMonitoringWithSingleSelectorTest() throws Exception { + // Setup selector + AppConfigurationKeyValueSelector selector = new AppConfigurationKeyValueSelector(); + selector.setKeyFilter(KEY_FILTER); + selector.setLabelFilter(LABEL_FILTER); + configStore.getSelects().add(selector); + + // Setup mocks + when(clientMock.collectionMonitoring(any(SettingSelector.class), any(Context.class))) + .thenReturn(collectionMonitoringMock); + + // Use reflection to test the private method + AzureAppConfigDataLoader loader = createLoader(); + List result = invokeCreateCollectionMonitoring(loader, clientMock); + + // Verify + assertNotNull(result); + assertEquals(1, result.size()); + + ArgumentCaptor selectorCaptor = ArgumentCaptor.forClass(SettingSelector.class); + verify(clientMock, times(1)).collectionMonitoring(selectorCaptor.capture(), any(Context.class)); + + SettingSelector capturedSelector = selectorCaptor.getValue(); + assertEquals(KEY_FILTER + "*", capturedSelector.getKeyFilter()); + assertEquals(LABEL_FILTER, capturedSelector.getLabelFilter()); + } + + @Test + public void createCollectionMonitoringWithMultipleSelectorsTest() throws Exception { + // Setup multiple selectors + AppConfigurationKeyValueSelector selector1 = new AppConfigurationKeyValueSelector(); + selector1.setKeyFilter("/app1/*"); + selector1.setLabelFilter("dev"); + configStore.getSelects().add(selector1); + + AppConfigurationKeyValueSelector selector2 = new AppConfigurationKeyValueSelector(); + selector2.setKeyFilter("/app2/*"); + selector2.setLabelFilter("prod"); + configStore.getSelects().add(selector2); + + // Setup mocks + when(clientMock.collectionMonitoring(any(SettingSelector.class), any(Context.class))) + .thenReturn(collectionMonitoringMock); + + // Test + AzureAppConfigDataLoader loader = createLoader(); + List result = invokeCreateCollectionMonitoring(loader, clientMock); + + // Verify - should create collection monitoring for both selectors + assertNotNull(result); + assertEquals(2, result.size()); + verify(clientMock, times(2)).collectionMonitoring(any(SettingSelector.class), any(Context.class)); + } + + @Test + public void createCollectionMonitoringSkipsSnapshotsTest() throws Exception { + // Setup selector with snapshot + AppConfigurationKeyValueSelector snapshotSelector = new AppConfigurationKeyValueSelector(); + snapshotSelector.setSnapshotName("my-snapshot"); + configStore.getSelects().add(snapshotSelector); + + // Setup regular selector + AppConfigurationKeyValueSelector regularSelector = new AppConfigurationKeyValueSelector(); + regularSelector.setKeyFilter(KEY_FILTER); + regularSelector.setLabelFilter(LABEL_FILTER); + configStore.getSelects().add(regularSelector); + + // Setup mocks + when(clientMock.collectionMonitoring(any(SettingSelector.class), any(Context.class))) + .thenReturn(collectionMonitoringMock); + + // Test + AzureAppConfigDataLoader loader = createLoader(); + List result = invokeCreateCollectionMonitoring(loader, clientMock); + + // Verify - snapshot should be skipped, only regular selector should be processed + assertNotNull(result); + assertEquals(1, result.size()); + verify(clientMock, times(1)).collectionMonitoring(any(SettingSelector.class), any(Context.class)); + } + + @Test + public void createCollectionMonitoringWithMultipleLabelsTest() throws Exception { + // Setup selector with multiple labels + AppConfigurationKeyValueSelector selector = new AppConfigurationKeyValueSelector(); + selector.setKeyFilter(KEY_FILTER); + selector.setLabelFilter("dev,prod,test"); + configStore.getSelects().add(selector); + + // Setup mocks + when(clientMock.collectionMonitoring(any(SettingSelector.class), any(Context.class))) + .thenReturn(collectionMonitoringMock); + + // Test + AzureAppConfigDataLoader loader = createLoader(); + List result = invokeCreateCollectionMonitoring(loader, clientMock); + + // Verify - should create collection monitoring for each label + assertNotNull(result); + assertEquals(3, result.size()); + verify(clientMock, times(3)).collectionMonitoring(any(SettingSelector.class), any(Context.class)); + } + + @Test + public void refreshAllEnabledUsesCollectionMonitoringTest() throws Exception { + // Setup monitoring with refreshAll enabled + AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + monitoring.setEnabled(true); + monitoring.setRefreshAll(true); + configStore.setMonitoring(monitoring); + + // Setup selector + AppConfigurationKeyValueSelector selector = new AppConfigurationKeyValueSelector(); + selector.setKeyFilter(KEY_FILTER); + selector.setLabelFilter(LABEL_FILTER); + configStore.getSelects().add(selector); + + // Setup mocks + when(clientMock.collectionMonitoring(any(SettingSelector.class), any(Context.class))) + .thenReturn(collectionMonitoringMock); + + // Test - verify that collection monitoring is created when refreshAll is enabled + AzureAppConfigDataLoader loader = createLoader(); + List result = invokeCreateCollectionMonitoring(loader, clientMock); + + // Verify collection monitoring was created + assertNotNull(result); + assertEquals(1, result.size()); + verify(clientMock, times(1)).collectionMonitoring(any(SettingSelector.class), any(Context.class)); + } + + @Test + public void refreshAllDisabledUsesWatchKeysTest() throws Exception { + // Setup monitoring with refreshAll disabled (traditional watch keys) + AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); + monitoring.setEnabled(true); + monitoring.setRefreshAll(false); + + // Add trigger for traditional watch key + AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); + trigger.setKey("sentinel"); + trigger.setLabel("prod"); + monitoring.setTriggers(List.of(trigger)); + + configStore.setMonitoring(monitoring); + + // Setup selector + AppConfigurationKeyValueSelector selector = new AppConfigurationKeyValueSelector(); + selector.setKeyFilter(KEY_FILTER); + selector.setLabelFilter(LABEL_FILTER); + configStore.getSelects().add(selector); + + // Verify that when refreshAll is false, triggers are configured + // The actual validation happens in validateAndInit which is called during load + assertEquals(1, monitoring.getTriggers().size()); + assertEquals("sentinel", monitoring.getTriggers().get(0).getKey()); + } + + // Helper methods + + private AzureAppConfigDataLoader createLoader() { + org.springframework.boot.logging.DeferredLogFactory logFactory = Mockito.mock(org.springframework.boot.logging.DeferredLogFactory.class); + when(logFactory.getLog(any(Class.class))).thenReturn(new org.springframework.boot.logging.DeferredLog()); + return new AzureAppConfigDataLoader(logFactory); + } + + private List invokeCreateCollectionMonitoring( + AzureAppConfigDataLoader loader, AppConfigurationReplicaClient client) throws Exception { + // Set resource field in the loader using reflection + java.lang.reflect.Field resourceField = AzureAppConfigDataLoader.class.getDeclaredField("resource"); + resourceField.setAccessible(true); + resourceField.set(loader, resource); + + // Set requestContext field (it can be null for this test) + java.lang.reflect.Field requestContextField = AzureAppConfigDataLoader.class.getDeclaredField("requestContext"); + requestContextField.setAccessible(true); + requestContextField.set(loader, Context.NONE); + + // Use reflection to invoke private method + java.lang.reflect.Method method = AzureAppConfigDataLoader.class + .getDeclaredMethod("createCollectionMonitoring", AppConfigurationReplicaClient.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + List result = (List) method.invoke(loader, client); + return result; + } +} From e4cac36a593e47ed773e47d4c2f4106e308a57d3 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 9 Jan 2026 09:14:41 -0800 Subject: [PATCH 10/13] Update AzureAppConfigDataLoader.java --- .../config/implementation/AzureAppConfigDataLoader.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 2394eb10f626..7997d6f92e77 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 @@ -2,11 +2,10 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; - 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; @@ -23,6 +22,7 @@ import com.azure.core.util.Context; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring; @@ -166,7 +166,7 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour if (Boolean.TRUE.equals(monitoring.getRefreshAll())) { // Use collection monitoring for refresh List collectionMonitoringList = createCollectionMonitoring(currentClient); - storeState.setState(resource.getEndpoint(), null, collectionMonitoringList, monitoring.getRefreshInterval()); + storeState.setState(resource.getEndpoint(), Collections.emptyList(), collectionMonitoringList, monitoring.getRefreshInterval()); } else { // Use traditional watch key monitoring List watchKeysSettings = monitoring.getTriggers().stream() From 6c19001749fa907d73e33fa87c14cdb3bb1968ad Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 13 Jan 2026 09:41:06 -0800 Subject: [PATCH 11/13] Fixing Formatting --- .../AppConfigurationRefreshUtil.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) 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 bfce8b93ebf4..b17a6acbefda 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 @@ -2,6 +2,8 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; + import java.time.Duration; import java.time.Instant; import java.util.List; @@ -14,7 +16,6 @@ import com.azure.core.exception.HttpResponseException; import com.azure.core.util.Context; import com.azure.data.appconfiguration.models.ConfigurationSetting; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlagState; @@ -81,7 +82,7 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF pushRefresh = true; } Context context = new Context("refresh", true).addData(PUSH_REFRESH, pushRefresh); - + clientFactory.findActiveClients(originEndpoint); if (monitor.isEnabled() && StateHolder.getLoadState(originEndpoint)) { @@ -140,14 +141,14 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF * @return the eventData if refresh is needed, null otherwise */ private RefreshEventData executeRefreshWithRetry( - AppConfigurationReplicaClientFactory clientFactory, - String originEndpoint, - RefreshOperation operation, - RefreshEventData eventData, - Context context, - String checkType) { + 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); @@ -225,7 +226,7 @@ private static void refreshWithTime(AppConfigurationReplicaClient client, State throws AppConfigurationStatusException { if (Instant.now().isAfter(state.getNextRefreshCheck())) { replicaLookUp.updateAutoFailoverEndpoints(); - + // Check collection monitoring first if configured if (state.getCollectionWatchKeys() != null && !state.getCollectionWatchKeys().isEmpty()) { refreshWithoutTimeCollectionMonitoring(client, state.getCollectionWatchKeys(), eventData, context); From d228a6bcf5002dc42fa8062796eae3d56d324888 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Tue, 13 Jan 2026 09:42:18 -0800 Subject: [PATCH 12/13] Update AppConfigurationReplicaClient.java --- .../config/implementation/AppConfigurationReplicaClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 93aadc0dadba..f5e29e2f820b 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 @@ -187,7 +187,7 @@ CollectionMonitoring collectionMonitoring(SettingSelector settingSelector, Conte * * @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 */ CollectionMonitoring listFeatureFlags(SettingSelector settingSelector, Context context) From c4c55f66cfc7e55b5917e5e7fa76d61bc3ce9d63 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 16 Jan 2026 15:13:48 -0800 Subject: [PATCH 13/13] Code Review comments --- .../AppConfigurationRefreshUtil.java | 15 ++++---- .../AppConfigurationReplicaClient.java | 10 +++--- ...ppConfigurationSnapshotPropertySource.java | 4 +-- .../AzureAppConfigDataLoader.java | 23 ++++++------ .../implementation/FeatureFlagClient.java | 19 +++++----- .../config/implementation/State.java | 10 +++--- .../config/implementation/StateHolder.java | 6 ++-- ...java => WatchedConfigurationSettings.java} | 16 ++++----- .../feature/FeatureFlagState.java | 8 ++--- .../AppConfigurationStoreMonitoring.java | 25 ++----------- .../AppConfigurationRefreshUtilTest.java | 24 ++++++------- .../AppConfigurationReplicaClientTest.java | 29 +++++++-------- .../AzureAppConfigDataLoaderTest.java | 22 ++++++------ .../implementation/FeatureFlagClientTest.java | 36 +++++++++---------- .../AppConfigurationStoreMonitoringTest.java | 26 +------------- 15 files changed, 112 insertions(+), 161 deletions(-) rename sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/configuration/{CollectionMonitoring.java => WatchedConfigurationSettings.java} (63%) 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 b17a6acbefda..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,7 +17,7 @@ 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.CollectionMonitoring; +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.properties.AppConfigurationStoreMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification; @@ -228,8 +228,9 @@ private static void refreshWithTime(AppConfigurationReplicaClient client, State replicaLookUp.updateAutoFailoverEndpoints(); // Check collection monitoring first if configured - if (state.getCollectionWatchKeys() != null && !state.getCollectionWatchKeys().isEmpty()) { - refreshWithoutTimeCollectionMonitoring(client, state.getCollectionWatchKeys(), eventData, context); + 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); @@ -276,9 +277,9 @@ private static void refreshWithoutTime(AppConfigurationReplicaClient client, Lis * @throws AppConfigurationStatusException if there's an error during the refresh check */ private static void refreshWithoutTimeCollectionMonitoring(AppConfigurationReplicaClient client, - List collectionWatchKeys, RefreshEventData eventData, Context context) + List collectionWatchKeys, RefreshEventData eventData, Context context) throws AppConfigurationStatusException { - for (CollectionMonitoring collectionMonitoring : collectionWatchKeys) { + for (WatchedConfigurationSettings collectionMonitoring : collectionWatchKeys) { if (client.checkWatchKeys(collectionMonitoring.getSettingSelector(), context)) { String eventDataInfo = collectionMonitoring.getSettingSelector().getKeyFilter(); @@ -311,7 +312,7 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl if (date.isAfter(state.getNextRefreshCheck())) { replicaLookUp.updateAutoFailoverEndpoints(); - for (CollectionMonitoring featureFlags : state.getWatchKeys()) { + for (WatchedConfigurationSettings featureFlags : state.getWatchKeys()) { if (client.checkWatchKeys(featureFlags.getSettingSelector(), context)) { // Only one refresh event needs to be called to update all of the // stores, not one for each. @@ -340,7 +341,7 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagState watchKeys, RefreshEventData eventData, Context context) throws AppConfigurationStatusException { - for (CollectionMonitoring featureFlags : watchKeys.getWatchKeys()) { + for (WatchedConfigurationSettings featureFlags : watchKeys.getWatchKeys()) { if (client.checkWatchKeys(featureFlags.getSettingSelector(), context)) { // Only one refresh event needs to be called to update all of the // stores, not one for each. 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 f5e29e2f820b..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.configuration.CollectionMonitoring; +import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings; import io.netty.handler.codec.http.HttpResponseStatus; @@ -159,7 +159,7 @@ List listSettings(SettingSelector settingSelector, Context * @return CollectionMonitoring containing the retrieved configuration settings and match conditions * @throws HttpResponseException if the request fails */ - CollectionMonitoring collectionMonitoring(SettingSelector settingSelector, Context context) { + WatchedConfigurationSettings collectionMonitoring(SettingSelector settingSelector, Context context) { List configurationSettings = new ArrayList<>(); List checks = new ArrayList<>(); try { @@ -174,7 +174,7 @@ CollectionMonitoring collectionMonitoring(SettingSelector settingSelector, Conte // Needs to happen after or we don't know if the request succeeded or failed. this.failedAttempts = 0; settingSelector.setMatchConditions(checks); - return new CollectionMonitoring(settingSelector, configurationSettings); + return new WatchedConfigurationSettings(settingSelector, configurationSettings); } catch (HttpResponseException e) { throw handleHttpResponseException(e); } catch (UncheckedIOException e) { @@ -190,7 +190,7 @@ CollectionMonitoring collectionMonitoring(SettingSelector settingSelector, Conte * @return CollectionMonitoring containing the retrieved feature flags and match conditions * @throws HttpResponseException if the request fails */ - CollectionMonitoring listFeatureFlags(SettingSelector settingSelector, Context context) + WatchedConfigurationSettings listFeatureFlags(SettingSelector settingSelector, Context context) throws HttpResponseException { List configurationSettings = new ArrayList<>(); List checks = new ArrayList<>(); @@ -207,7 +207,7 @@ CollectionMonitoring listFeatureFlags(SettingSelector settingSelector, Context c // Needs to happen after or we don't know if the request succeeded or failed. this.failedAttempts = 0; settingSelector.setMatchConditions(checks); - return new CollectionMonitoring(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 f31aa638a87b..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.configuration.CollectionMonitoring; +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); - CollectionMonitoring featureFlags = new CollectionMonitoring(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 7997d6f92e77..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 @@ -2,6 +2,8 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; +import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; + import java.io.IOException; import java.time.Instant; import java.util.ArrayList; @@ -22,8 +24,7 @@ import com.azure.core.util.Context; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; -import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH; -import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; +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; @@ -153,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(); @@ -163,9 +164,9 @@ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResour if (monitoring.isEnabled()) { // Check if refreshAll is enabled - if so, use collection monitoring - if (Boolean.TRUE.equals(monitoring.getRefreshAll())) { + if (monitoring.getTriggers().size() == 0) { // Use collection monitoring for refresh - List collectionMonitoringList = createCollectionMonitoring(currentClient); + List collectionMonitoringList = getWatchedConfigurationSettings(currentClient); storeState.setState(resource.getEndpoint(), Collections.emptyList(), collectionMonitoringList, monitoring.getRefreshInterval()); } else { // Use traditional watch key monitoring @@ -270,13 +271,13 @@ private List createSettings(AppConfigurationRepl * @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); } @@ -292,9 +293,9 @@ private List createFeatureFlags(AppConfigurationReplicaCli * @return a list of CollectionMonitoring for configuration settings * @throws Exception creating collection monitoring failed */ - private List createCollectionMonitoring(AppConfigurationReplicaClient client) + private List getWatchedConfigurationSettings(AppConfigurationReplicaClient client) throws Exception { - List collectionMonitoringList = new ArrayList<>(); + List collectionMonitoringList = new ArrayList<>(); List selects = resource.getSelects(); List profiles = resource.getProfiles().getActive(); @@ -310,7 +311,7 @@ private List createCollectionMonitoring(AppConfigurationRe .setKeyFilter(selectedKeys.getKeyFilter() + "*") .setLabelFilter(label); - CollectionMonitoring collectionMonitoring = client.collectionMonitoring(settingSelector, requestContext); + WatchedConfigurationSettings collectionMonitoring = client.collectionMonitoring(settingSelector, requestContext); collectionMonitoringList.add(collectionMonitoring); } } 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 5e0e309635e2..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.configuration.CollectionMonitoring; +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 replic SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter).setLabelFilter(label); context.addData("FeatureFlagTracing", tracing); - CollectionMonitoring 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(CollectionMonitoring features, String endpoint) { - List loadedFeatureFlags = new ArrayList<>(); - loadedFeatureFlags.add(features); - + WatchedConfigurationSettings proccessFeatureFlags(WatchedConfigurationSettings features, String endpoint) { // Reading In Features - for (ConfigurationSetting setting : features.getConfigurations()) { + 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(CollectionMonitoring features, S 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 3cb4bf90af00..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,7 +6,7 @@ import java.util.List; import com.azure.data.appconfiguration.models.ConfigurationSetting; -import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; +import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings; /** * Immutable representation of the refresh state for an Azure App Configuration store. @@ -24,7 +24,7 @@ class State { private final List watchKeys; /** Collection monitoring configurations that can trigger refresh events. */ - private final List collectionWatchKeys; + private final List collectionWatchKeys; /** The next time this store should be checked for refresh. */ private final Instant nextRefreshCheck; @@ -56,7 +56,7 @@ class State { * @param refreshInterval refresh interval in seconds * @param originEndpoint the endpoint URL of the configuration store */ - State(List watchKeys, List collectionWatchKeys, int refreshInterval, String originEndpoint) { + State(List watchKeys, List collectionWatchKeys, int refreshInterval, String originEndpoint) { this(watchKeys, collectionWatchKeys, refreshInterval, originEndpoint, Instant.now().plusSeconds(refreshInterval), 1); } @@ -92,7 +92,7 @@ class State { * @param nextRefreshCheck the next time to check for refresh * @param refreshAttempt the current refresh attempt count */ - private State(List watchKeys, List collectionWatchKeys, + private State(List watchKeys, List collectionWatchKeys, int refreshInterval, String originEndpoint, Instant nextRefreshCheck, int refreshAttempt) { this.watchKeys = watchKeys; this.collectionWatchKeys = collectionWatchKeys; @@ -114,7 +114,7 @@ public List getWatchKeys() { * Gets the collection monitoring configurations for this store. * @return the list of collection monitoring configurations, or null if not configured */ - public List getCollectionWatchKeys() { + public List getCollectionWatchKeys() { return collectionWatchKeys; } 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 14edc1e95516..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,7 +11,7 @@ import java.util.concurrent.ConcurrentHashMap; import com.azure.data.appconfiguration.models.ConfigurationSetting; -import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; +import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.WatchedConfigurationSettings; import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlagState; /** @@ -127,7 +127,7 @@ void setState(String originEndpoint, List watchKeys, Durat * @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) { + void setState(String originEndpoint, List watchKeys, List collectionWatchKeys, Duration duration) { state.put(originEndpoint, new State(watchKeys, collectionWatchKeys, Math.toIntExact(duration.getSeconds()), originEndpoint)); } @@ -137,7 +137,7 @@ void setState(String originEndpoint, List watchKeys, List< * @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)); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/configuration/CollectionMonitoring.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/configuration/WatchedConfigurationSettings.java similarity index 63% rename from sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/configuration/CollectionMonitoring.java rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/configuration/WatchedConfigurationSettings.java index aa70f07f6129..782efef549f5 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/configuration/CollectionMonitoring.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/configuration/WatchedConfigurationSettings.java @@ -7,15 +7,15 @@ import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; -public class CollectionMonitoring { +public class WatchedConfigurationSettings { private SettingSelector settingSelector; - private List configurations; + private List configurationSettings; - public CollectionMonitoring(SettingSelector settingSelector, List configurations) { + public WatchedConfigurationSettings(SettingSelector settingSelector, List configurationSettings) { this.settingSelector = settingSelector; - this.configurations = configurations; + this.configurationSettings = configurationSettings; } /** @@ -35,15 +35,15 @@ public void setSettingSelector(SettingSelector settingSelector) { /** * @return the featureFlags */ - public List getConfigurations() { - return configurations; + public List getConfigurationSettings() { + return configurationSettings; } /** * @param configurations the configurations to set */ - public void setConfigurations(List configurations) { - this.configurations = configurations; + 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 34124e900d36..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,17 +5,17 @@ import java.time.Instant; import java.util.List; -import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; +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; @@ -30,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 d01b219173ac..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 @@ -36,8 +36,6 @@ public final class AppConfigurationStoreMonitoring { */ private List triggers = new ArrayList<>(); - private Boolean refreshAll = false; - /** * Validation tokens for push notification requests. */ @@ -116,32 +114,15 @@ public void setPushNotification(PushNotification pushNotification) { this.pushNotification = pushNotification; } - /** - * @return the refreshAll - */ - public Boolean getRefreshAll() { - return refreshAll; - } - - /** - * @param refreshAll the refreshAll to set - */ - public void setRefreshAll(Boolean refreshAll) { - this.refreshAll = refreshAll; - } - /** * Validates refreshIntervals are at least 1 second, and if enabled triggers are valid. */ @PostConstruct void validateAndInit() { if (enabled) { - // Triggers are required unless refreshAll is enabled (which uses collection monitoring instead) - if (!Boolean.TRUE.equals(refreshAll)) { - Assert.notEmpty(triggers, "Triggers need to be set if refresh is enabled and refreshAll is not enabled."); - for (AppConfigurationStoreTrigger trigger : triggers) { - trigger.validateAndInit(); - } + // Triggers are not required defaults to use collection monitoring if not set + for (AppConfigurationStoreTrigger trigger : triggers) { + trigger.validateAndInit(); } } Assert.isTrue(refreshInterval.getSeconds() >= 1, "Minimum refresh interval time is 1 Second."); 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 8c317029b9b7..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 @@ -34,7 +34,7 @@ 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.CollectionMonitoring; +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.properties.AppConfigurationStoreMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.AccessToken; @@ -142,7 +142,7 @@ public void refreshWithoutTimeWatchKeyConfigStoreWatchKeyNoChange(TestInfo testI when(clientMock.getEndpoint()).thenReturn(endpoint); FeatureFlagState newState = new FeatureFlagState( - List.of(new CollectionMonitoring(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. @@ -190,7 +190,7 @@ public void refreshWithoutTimeFeatureFlagNoChange(TestInfo testInfo) { when(clientMock.getEndpoint()).thenReturn(endpoint); FeatureFlagState newState = new FeatureFlagState( - List.of(new CollectionMonitoring(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. @@ -210,7 +210,7 @@ public void refreshWithoutTimeFeatureFlagEtagChanged(TestInfo testInfo) { endpoint = testInfo.getDisplayName() + ".azconfig.io"; when(clientMock.getEndpoint()).thenReturn(endpoint); - CollectionMonitoring featureFlags = new CollectionMonitoring(new SettingSelector(), watchKeysFeatureFlags); + WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(new SettingSelector(), watchKeysFeatureFlags); FeatureFlagState newState = new FeatureFlagState(List.of(featureFlags), Math.toIntExact(Duration.ofMinutes(10).getSeconds()), endpoint); @@ -512,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 CollectionMonitoring(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. @@ -538,7 +538,7 @@ public void refreshStoresCheckFeatureFlagTestTriggerRefresh(TestInfo testInfo) { setupFeatureFlagLoad(); when(clientOriginMock.checkWatchKeys(Mockito.any(), Mockito.any(Context.class))).thenReturn(true); - CollectionMonitoring featureFlags = new CollectionMonitoring(new SettingSelector(), watchKeysFeatureFlags); + WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(new SettingSelector(), watchKeysFeatureFlags); FeatureFlagState newState = new FeatureFlagState(List.of(featureFlags), Math.toIntExact(Duration.ofMinutes(-1).getSeconds()), endpoint); @@ -600,14 +600,13 @@ public void refreshAllWithCollectionMonitoringTest(TestInfo testInfo) { // Test that when refreshAll is enabled, collection monitoring is used instead of watch keys endpoint = testInfo.getDisplayName() + ".azconfig.io"; - monitoring.setRefreshAll(true); 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 - CollectionMonitoring collectionMonitoring = new CollectionMonitoring( + 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); @@ -640,7 +639,6 @@ public void refreshAllWithNullWatchKeysTest(TestInfo testInfo) { // Test that when refreshAll is enabled with null watchKeys, collection monitoring is still used endpoint = testInfo.getDisplayName() + ".azconfig.io"; - monitoring.setRefreshAll(true); when(connectionManagerMock.getMonitoring()).thenReturn(monitoring); FeatureFlagStore disabledFeatureStore = new FeatureFlagStore(); disabledFeatureStore.setEnabled(false); @@ -650,7 +648,7 @@ public void refreshAllWithNullWatchKeysTest(TestInfo testInfo) { .thenReturn(clientOriginMock); // Set up state with null watch keys but valid collection monitoring - CollectionMonitoring collectionMonitoring = new CollectionMonitoring( + 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); @@ -678,7 +676,6 @@ public void collectionMonitoringNoChangeTest(TestInfo testInfo) { // Test that collection monitoring correctly detects no change endpoint = testInfo.getDisplayName() + ".azconfig.io"; - monitoring.setRefreshAll(true); when(connectionManagerMock.getMonitoring()).thenReturn(monitoring); FeatureFlagStore disabledFeatureStore = new FeatureFlagStore(); disabledFeatureStore.setEnabled(false); @@ -687,7 +684,7 @@ public void collectionMonitoringNoChangeTest(TestInfo testInfo) { when(clientFactoryMock.getNextActiveClient(Mockito.eq(endpoint), Mockito.booleanThat(value -> true))) .thenReturn(clientOriginMock); - CollectionMonitoring collectionMonitoring = new CollectionMonitoring( + WatchedConfigurationSettings collectionMonitoring = new WatchedConfigurationSettings( new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), generateWatchKeys()); State state = new State(null, List.of(collectionMonitoring), @@ -715,13 +712,12 @@ public void collectionMonitoringWithChangeDetectedTest(TestInfo testInfo) { // Test that collection monitoring correctly detects changes endpoint = testInfo.getDisplayName() + ".azconfig.io"; - monitoring.setRefreshAll(true); 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); - CollectionMonitoring collectionMonitoring = new CollectionMonitoring( + WatchedConfigurationSettings collectionMonitoring = new WatchedConfigurationSettings( new SettingSelector().setKeyFilter(KEY_FILTER).setLabelFilter(EMPTY_LABEL), generateWatchKeys()); State state = new State(null, List.of(collectionMonitoring), 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 4ef1855d75b1..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 @@ -2,6 +2,15 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import java.io.IOException; import java.io.UncheckedIOException; import java.net.UnknownHostException; @@ -11,18 +20,10 @@ import java.util.function.Supplier; 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.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.Mockito; -import static org.mockito.Mockito.reset; -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; @@ -44,7 +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.CollectionMonitoring; +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).getConfigurations()); + assertEquals(configurations, client.listFeatureFlags(new SettingSelector(), contextMock).getConfigurationSettings()); when(clientMock.listConfigurationSettings(Mockito.any(), Mockito.any())).thenThrow(exceptionMock); when(exceptionMock.getResponse()).thenReturn(responseMock); @@ -367,11 +368,11 @@ public void collectionMonitoringTest() { .thenReturn(new PagedIterable<>(pagedFlux)); SettingSelector selector = new SettingSelector().setKeyFilter("*"); - CollectionMonitoring result = client.collectionMonitoring(selector, contextMock); + WatchedConfigurationSettings result = client.collectionMonitoring(selector, contextMock); - assertEquals(2, result.getConfigurations().size()); - assertEquals("key1", result.getConfigurations().get(0).getKey()); - assertEquals("key2", result.getConfigurations().get(1).getKey()); + assertEquals(2, result.getConfigurationSettings().size()); + assertEquals("key1", result.getConfigurationSettings().get(0).getKey()); + assertEquals("key2", result.getConfigurationSettings().get(1).getKey()); assertEquals(1, result.getSettingSelector().getMatchConditions().size()); assertEquals("test-etag-value", result.getSettingSelector().getMatchConditions().get(0).getIfNoneMatch()); assertEquals(0, client.getFailedAttempts()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java index ea3b1ddcd0c2..31bcb5966f75 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoaderTest.java @@ -25,7 +25,7 @@ import com.azure.core.util.Context; import com.azure.data.appconfiguration.models.SettingSelector; -import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; +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.AppConfigurationStoreTrigger; @@ -38,7 +38,7 @@ public class AzureAppConfigDataLoaderTest { private AppConfigurationReplicaClient clientMock; @Mock - private CollectionMonitoring collectionMonitoringMock; + private WatchedConfigurationSettings collectionMonitoringMock; private AzureAppConfigDataResource resource; @@ -93,7 +93,7 @@ public void createCollectionMonitoringWithSingleSelectorTest() throws Exception // Use reflection to test the private method AzureAppConfigDataLoader loader = createLoader(); - List result = invokeCreateCollectionMonitoring(loader, clientMock); + List result = invokeGetWatchedConfigurationSettings(loader, clientMock); // Verify assertNotNull(result); @@ -126,7 +126,7 @@ public void createCollectionMonitoringWithMultipleSelectorsTest() throws Excepti // Test AzureAppConfigDataLoader loader = createLoader(); - List result = invokeCreateCollectionMonitoring(loader, clientMock); + List result = invokeGetWatchedConfigurationSettings(loader, clientMock); // Verify - should create collection monitoring for both selectors assertNotNull(result); @@ -153,7 +153,7 @@ public void createCollectionMonitoringSkipsSnapshotsTest() throws Exception { // Test AzureAppConfigDataLoader loader = createLoader(); - List result = invokeCreateCollectionMonitoring(loader, clientMock); + List result = invokeGetWatchedConfigurationSettings(loader, clientMock); // Verify - snapshot should be skipped, only regular selector should be processed assertNotNull(result); @@ -175,7 +175,7 @@ public void createCollectionMonitoringWithMultipleLabelsTest() throws Exception // Test AzureAppConfigDataLoader loader = createLoader(); - List result = invokeCreateCollectionMonitoring(loader, clientMock); + List result = invokeGetWatchedConfigurationSettings(loader, clientMock); // Verify - should create collection monitoring for each label assertNotNull(result); @@ -188,7 +188,6 @@ public void refreshAllEnabledUsesCollectionMonitoringTest() throws Exception { // Setup monitoring with refreshAll enabled AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); monitoring.setEnabled(true); - monitoring.setRefreshAll(true); configStore.setMonitoring(monitoring); // Setup selector @@ -203,7 +202,7 @@ public void refreshAllEnabledUsesCollectionMonitoringTest() throws Exception { // Test - verify that collection monitoring is created when refreshAll is enabled AzureAppConfigDataLoader loader = createLoader(); - List result = invokeCreateCollectionMonitoring(loader, clientMock); + List result = invokeGetWatchedConfigurationSettings(loader, clientMock); // Verify collection monitoring was created assertNotNull(result); @@ -216,7 +215,6 @@ public void refreshAllDisabledUsesWatchKeysTest() throws Exception { // Setup monitoring with refreshAll disabled (traditional watch keys) AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); monitoring.setEnabled(true); - monitoring.setRefreshAll(false); // Add trigger for traditional watch key AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); @@ -246,7 +244,7 @@ private AzureAppConfigDataLoader createLoader() { return new AzureAppConfigDataLoader(logFactory); } - private List invokeCreateCollectionMonitoring( + private List invokeGetWatchedConfigurationSettings( AzureAppConfigDataLoader loader, AppConfigurationReplicaClient client) throws Exception { // Set resource field in the loader using reflection java.lang.reflect.Field resourceField = AzureAppConfigDataLoader.class.getDeclaredField("resource"); @@ -260,10 +258,10 @@ private List invokeCreateCollectionMonitoring( // Use reflection to invoke private method java.lang.reflect.Method method = AzureAppConfigDataLoader.class - .getDeclaredMethod("createCollectionMonitoring", AppConfigurationReplicaClient.class); + .getDeclaredMethod("getWatchedConfigurationSettings", AppConfigurationReplicaClient.class); method.setAccessible(true); @SuppressWarnings("unchecked") - List result = (List) method.invoke(loader, client); + List result = (List) method.invoke(loader, client); return result; } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java index 1198b93c37b2..1a4f75d4c150 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClientTest.java @@ -36,7 +36,7 @@ import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagFilter; -import com.azure.spring.cloud.appconfiguration.config.implementation.configuration.CollectionMonitoring; +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.Variant; @@ -80,14 +80,14 @@ public void cleanup() throws Exception { @Test public void loadFeatureFlagsTestNoFeatureFlags() { List settings = List.of(new ConfigurationSetting().setKey("FakeKey")); - CollectionMonitoring featureFlags = new CollectionMonitoring(null, settings); + WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); - assertEquals("FakeKey", featureFlagsList.get(0).getConfigurations().get(0).getKey()); + assertEquals("FakeKey", featureFlagsList.get(0).getConfigurationSettings().get(0).getKey()); assertEquals(0, featureFlagClient.getFeatureFlags().size()); } @@ -95,15 +95,15 @@ public void loadFeatureFlagsTestNoFeatureFlags() { public void loadFeatureFlagsTestFeatureFlags() { List settings = List.of(new FeatureFlagConfigurationSetting("Alpha", false), new FeatureFlagConfigurationSetting("Beta", true)); - CollectionMonitoring featureFlags = new CollectionMonitoring(null, settings); + WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); - assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getConfigurations().get(0).getKey()); - assertEquals(".appconfig.featureflag/Beta", featureFlagsList.get(0).getConfigurations().get(1).getKey()); + assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getConfigurationSettings().get(0).getKey()); + assertEquals(".appconfig.featureflag/Beta", featureFlagsList.get(0).getConfigurationSettings().get(1).getKey()); assertEquals(2, featureFlagClient.getFeatureFlags().size()); } @@ -111,27 +111,27 @@ public void loadFeatureFlagsTestFeatureFlags() { public void loadFeatureFlagsTestMultipleLoads() { List settings = List.of(new FeatureFlagConfigurationSetting("Alpha", false), new FeatureFlagConfigurationSetting("Beta", true)); - CollectionMonitoring featureFlags = new CollectionMonitoring(null, settings); + WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); - assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getConfigurations().get(0).getKey()); - assertEquals(".appconfig.featureflag/Beta", featureFlagsList.get(0).getConfigurations().get(1).getKey()); + assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getConfigurationSettings().get(0).getKey()); + assertEquals(".appconfig.featureflag/Beta", featureFlagsList.get(0).getConfigurationSettings().get(1).getKey()); assertEquals(2, featureFlagClient.getFeatureFlags().size()); List settings2 = List.of(new FeatureFlagConfigurationSetting("Alpha", true), new FeatureFlagConfigurationSetting("Gamma", false)); - featureFlags = new CollectionMonitoring(null, settings2); + featureFlags = new WatchedConfigurationSettings(null, settings2); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); - assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getConfigurations().get(0).getKey()); - assertEquals(".appconfig.featureflag/Gamma", featureFlagsList.get(0).getConfigurations().get(1).getKey()); + assertEquals(".appconfig.featureflag/Alpha", featureFlagsList.get(0).getConfigurationSettings().get(0).getKey()); + assertEquals(".appconfig.featureflag/Gamma", featureFlagsList.get(0).getConfigurationSettings().get(1).getKey()); assertEquals(3, featureFlagClient.getFeatureFlags().size()); List features = featureFlagClient.getFeatureFlags(); assertTrue(features.get(0).isEnabled()); @@ -170,14 +170,14 @@ public void loadFeatureFlagsTestTargetingFilter() { targetingFilter.addParameter("Audience", parameters); targetingFlag.addClientFilter(targetingFilter); List settings = List.of(targetingFlag); - CollectionMonitoring featureFlags = new CollectionMonitoring(null, settings); + WatchedConfigurationSettings featureFlags = new WatchedConfigurationSettings(null, settings); when(clientMock.listFeatureFlags(Mockito.any(), Mockito.any(Context.class))).thenReturn(featureFlags); - List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, + List featureFlagsList = featureFlagClient.loadFeatureFlags(clientMock, null, emptyLabelList, contextMock); assertEquals(1, featureFlagsList.size()); assertEquals(featureFlags, featureFlagsList.get(0)); - assertEquals(".appconfig.featureflag/TargetingTest", featureFlagsList.get(0).getConfigurations().get(0).getKey()); + assertEquals(".appconfig.featureflag/TargetingTest", featureFlagsList.get(0).getConfigurationSettings().get(0).getKey()); assertEquals(1, featureFlagClient.getFeatureFlags().size()); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoringTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoringTest.java index 060e11ec0f53..9cb7b035a6c5 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoringTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/AppConfigurationStoreMonitoringTest.java @@ -18,15 +18,9 @@ public void validateAndInitTest() { AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); monitoring.validateAndInit(); - // Enabled throw error if no triggers - monitoring.setEnabled(true); - assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); - List triggers = new ArrayList<>(); monitoring.setTriggers(triggers); - assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); - AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); trigger.setKey("sentinal"); @@ -55,8 +49,7 @@ public void refreshAllEnabledWithoutTriggersTest() { // When refreshAll is enabled, triggers are not required AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); monitoring.setEnabled(true); - monitoring.setRefreshAll(true); - + // Should not throw an exception even with no triggers monitoring.validateAndInit(); @@ -66,27 +59,11 @@ public void refreshAllEnabledWithoutTriggersTest() { assertEquals("Minimum refresh interval time is 1 Second.", e.getMessage()); } - @Test - public void refreshAllDisabledRequiresTriggersTest() { - // When refreshAll is disabled or null, triggers are required - AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); - monitoring.setEnabled(true); - monitoring.setRefreshAll(false); - - // Should throw an exception with no triggers - assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); - - // Setting refreshAll to null should also require triggers - monitoring.setRefreshAll(null); - assertThrows(IllegalArgumentException.class, () -> monitoring.validateAndInit()); - } - @Test public void refreshAllWithTriggersTest() { // Even when refreshAll is enabled, having triggers should still be valid AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); monitoring.setEnabled(true); - monitoring.setRefreshAll(true); List triggers = new ArrayList<>(); AppConfigurationStoreTrigger trigger = new AppConfigurationStoreTrigger(); @@ -103,7 +80,6 @@ public void monitoringDisabledWithRefreshAllTest() { // When monitoring is disabled, refreshAll setting should not matter AppConfigurationStoreMonitoring monitoring = new AppConfigurationStoreMonitoring(); monitoring.setEnabled(false); - monitoring.setRefreshAll(true); // Should not throw an exception even with no triggers monitoring.validateAndInit();