diff --git a/sdk/spring/ci.yml b/sdk/spring/ci.yml index 7e54f31246d79..8a4b6244cd440 100644 --- a/sdk/spring/ci.yml +++ b/sdk/spring/ci.yml @@ -547,26 +547,26 @@ extends: - name: spring-cloud-azure-appconfiguration-config groupId: com.azure.spring safeName: springcloudazureappconfigurationconfig - skipPublishDocGithubIo: true - skipPublishDocMs: true + skipPublishDocGithubIo: false + skipPublishDocMs: false releaseInBatch: ${{ parameters.release_springcloudazureappconfigurationconfig }} - name: spring-cloud-azure-appconfiguration-config-web groupId: com.azure.spring safeName: springcloudazureappconfigurationconfigweb - skipPublishDocGithubIo: true - skipPublishDocMs: true + skipPublishDocGithubIo: false + skipPublishDocMs: false releaseInBatch: ${{ parameters.release_springcloudazureappconfigurationconfigweb }} - name: spring-cloud-azure-feature-management groupId: com.azure.spring safeName: springcloudazurefeaturemanagement - skipPublishDocGithubIo: true - skipPublishDocMs: true + skipPublishDocGithubIo: false + skipPublishDocMs: false releaseInBatch: ${{ parameters.release_springcloudazurefeaturemanagement }} - name: spring-cloud-azure-feature-management-web groupId: com.azure.spring safeName: springcloudazurefeaturemanagementweb - skipPublishDocGithubIo: true - skipPublishDocMs: true + skipPublishDocGithubIo: false + skipPublishDocMs: false releaseInBatch: ${{ parameters.release_springcloudazurefeaturemanagementweb }} - name: spring-cloud-azure-starter-appconfiguration-config groupId: com.azure.spring diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md index a601a2ee42f0e..b9c894a59e2a7 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md @@ -4,6 +4,9 @@ ### Features Added +* Added Auto fail over support. Will automatically find Azure App Configuration replica stores for provided store. The found replica stores will be used as fail over stores after all provided replicas have failed. +* Added property to disable auto fail over support `spring.cloud.azure.appconfiguration.stores[0].replica-discovery-enabled`. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationAutoConfiguration.java index 015d9e5ed2630..427bc85869f87 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationAutoConfiguration.java @@ -12,6 +12,7 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationPullRefresh; import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientFactory; +import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProviderProperties; @@ -45,9 +46,9 @@ public AppConfigurationWatchAutoConfiguration() { @Bean @ConditionalOnMissingBean AppConfigurationRefresh appConfigurationRefresh(AppConfigurationProperties properties, - AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory) { + AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, ReplicaLookUp replicaLookUp) { return new AppConfigurationPullRefresh(clientFactory, properties.getRefreshInterval(), - appProperties.getDefaultMinBackoff()); + appProperties.getDefaultMinBackoff(), replicaLookUp); } } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java index f17487acce07e..705ac305106c0 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java @@ -22,6 +22,7 @@ import org.springframework.util.StringUtils; 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.properties.AppConfigurationKeyValueSelector; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProviderProperties; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring; @@ -47,6 +48,8 @@ public final class AppConfigurationPropertySourceLocator implements PropertySour private final AppConfigurationReplicaClientFactory clientFactory; private final AppConfigurationKeyVaultClientFactory keyVaultClientFactory; + + private final ReplicaLookUp replicaLookUp; private Duration refreshInterval; @@ -62,12 +65,13 @@ public final class AppConfigurationPropertySourceLocator implements PropertySour */ public AppConfigurationPropertySourceLocator(AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, AppConfigurationKeyVaultClientFactory keyVaultClientFactory, - Duration refreshInterval, List configStores) { + Duration refreshInterval, List configStores, ReplicaLookUp replicaLookUp) { this.refreshInterval = refreshInterval; this.appProperties = appProperties; this.configStores = configStores; this.clientFactory = clientFactory; this.keyVaultClientFactory = keyVaultClientFactory; + this.replicaLookUp = replicaLookUp; BackoffTimeCalculator.setDefaults(appProperties.getDefaultMaxBackoff(), appProperties.getDefaultMinBackoff()); } @@ -77,6 +81,7 @@ public PropertySource locate(Environment environment) { if (!(environment instanceof ConfigurableEnvironment)) { return null; } + replicaLookUp.updateAutoFailoverEndpoints(); ConfigurableEnvironment env = (ConfigurableEnvironment) environment; boolean currentlyLoaded = env.getPropertySources().stream().anyMatch(source -> { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java index 570af2ae1746f..2bff2554316f3 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java @@ -19,6 +19,7 @@ import com.azure.spring.cloud.appconfiguration.config.AppConfigurationRefresh; import com.azure.spring.cloud.appconfiguration.config.AppConfigurationStoreHealth; 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.http.policy.BaseAppConfigurationPolicy; import reactor.core.publisher.Mono; @@ -42,6 +43,8 @@ public class AppConfigurationPullRefresh implements AppConfigurationRefresh, Env private final Duration refreshInterval; private List profiles; + + private final ReplicaLookUp replicaLookUp; /** * Component used for checking for and triggering configuration refreshes. @@ -51,10 +54,11 @@ public class AppConfigurationPullRefresh implements AppConfigurationRefresh, Env * @param defaultMinBackoff minimum time between backoff retries minimum backoff time */ public AppConfigurationPullRefresh(AppConfigurationReplicaClientFactory clientFactory, Duration refreshInterval, - Long defaultMinBackoff) { + Long defaultMinBackoff, ReplicaLookUp replicaLookUp) { this.defaultMinBackoff = defaultMinBackoff; this.refreshInterval = refreshInterval; this.clientFactory = clientFactory; + this.replicaLookUp = replicaLookUp; } @@ -101,7 +105,7 @@ private boolean refreshStores() { BaseAppConfigurationPolicy.setWatchRequests(true); try { RefreshEventData eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactory, - refreshInterval, profiles, defaultMinBackoff); + refreshInterval, profiles, defaultMinBackoff, replicaLookUp); if (eventData.getDoRefresh()) { publisher.publishEvent(new RefreshEvent(this, eventData, eventData.getMessage())); return true; 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 a565ff5791368..875891d561046 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 @@ -15,9 +15,11 @@ import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; +import com.azure.core.exception.HttpResponseException; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; import com.azure.data.appconfiguration.models.SettingSelector; +import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.BaseAppConfigurationPolicy; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector; @@ -34,7 +36,7 @@ class AppConfigurationRefreshUtil { * @return If a refresh event is called. */ static RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientFactory, - Duration refreshInterval, List profiles, Long defaultMinBackoff) { + Duration refreshInterval, List profiles, Long defaultMinBackoff, ReplicaLookUp replicaLookUp) { RefreshEventData eventData = new RefreshEventData(); BaseAppConfigurationPolicy.setWatchRequests(true); @@ -62,14 +64,14 @@ static RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory if (monitor.isEnabled() && StateHolder.getLoadState(originEndpoint)) { for (AppConfigurationReplicaClient client : clients) { try { - refreshWithTime(client, StateHolder.getState(originEndpoint), monitor.getRefreshInterval(), eventData); + refreshWithTime(client, StateHolder.getState(originEndpoint), monitor.getRefreshInterval(), eventData, replicaLookUp); 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 (AppConfigurationStatusException e) { + } catch (HttpResponseException e) { LOGGER.warn( "Failed attempting to connect to " + client.getEndpoint() + " during refresh check."); @@ -87,14 +89,14 @@ static RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory try { refreshWithTimeFeatureFlags(client, featureStore, StateHolder.getStateFeatureFlag(originEndpoint), - monitor.getFeatureFlagRefreshInterval(), eventData, profiles); + monitor.getFeatureFlagRefreshInterval(), eventData, profiles, replicaLookUp); 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 (AppConfigurationStatusException e) { + } catch (HttpResponseException e) { LOGGER.warn( "Failed attempting to connect to " + client.getEndpoint() + " during refresh check."); @@ -165,9 +167,9 @@ private static boolean refreshStoreFeatureFlagCheck(FeatureFlagStore featureStor * @param eventData Info for this refresh event. */ private static void refreshWithTime(AppConfigurationReplicaClient client, State state, Duration refreshInterval, - RefreshEventData eventData) throws AppConfigurationStatusException { + RefreshEventData eventData, ReplicaLookUp replicaLookUp) throws AppConfigurationStatusException { if (Instant.now().isAfter(state.getNextRefreshCheck())) { - + replicaLookUp.updateAutoFailoverEndpoints(); refreshWithoutTime(client, state.getWatchKeys(), eventData); StateHolder.getCurrentState().updateStateRefresh(state, refreshInterval); @@ -198,11 +200,11 @@ private static void refreshWithoutTime(AppConfigurationReplicaClient client, Lis } private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagStore featureStore, - State state, Duration refreshInterval, RefreshEventData eventData, List profiles) + State state, Duration refreshInterval, RefreshEventData eventData, List profiles, ReplicaLookUp replicaLookUp) throws AppConfigurationStatusException { Instant date = Instant.now(); if (date.isAfter(state.getNextRefreshCheck())) { - + replicaLookUp.updateAutoFailoverEndpoints(); int watchedKeySize = 0; for (FeatureFlagKeyValueSelector watchKey : featureStore.getSelects()) { 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 bfd8c54713357..37bfa6028d297 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 @@ -116,8 +116,9 @@ List listSettings(SettingSelector settingSelector) List configurationSettings = new ArrayList<>(); try { PagedIterable settings = client.listConfigurationSettings(settingSelector); - this.failedAttempts = 0; settings.forEach(setting -> configurationSettings.add(NormalizeNull.normalizeNullLabel(setting))); + // Needs to happen after or we don't know if the request succeeded or failed. + this.failedAttempts = 0; return configurationSettings; } catch (HttpResponseException e) { if (e.getResponse() != null) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java index 1c25cecfa5671..4f3fd987cc2f2 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java @@ -7,6 +7,7 @@ import java.util.Map; import com.azure.spring.cloud.appconfiguration.config.AppConfigurationStoreHealth; +import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; /** @@ -25,12 +26,12 @@ public class AppConfigurationReplicaClientFactory { * @param configStores configuration info for config stores */ public AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder, - List configStores) { + List configStores, ReplicaLookUp replicaLookUp) { this.configStores = configStores; if (CONNECTIONS.size() == 0) { for (ConfigStore store : configStores) { - ConnectionManager manager = new ConnectionManager(clientBuilder, store); - CONNECTIONS.put(manager.getOriginEndpoint(), manager); + ConnectionManager manager = new ConnectionManager(clientBuilder, store, replicaLookUp); + CONNECTIONS.put(manager.getMainEndpoint(), manager); } } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java index 4d271495ed2b6..bb44bee61445c 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java @@ -2,6 +2,8 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation; +import java.net.MalformedURLException; +import java.net.URL; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -21,6 +23,7 @@ import com.azure.core.http.policy.RetryPolicy; import com.azure.core.http.policy.RetryStrategy; import com.azure.core.util.Configuration; +import com.azure.core.util.CoreUtils; import com.azure.data.appconfiguration.ConfigurationClientBuilder; import com.azure.identity.ManagedIdentityCredentialBuilder; import com.azure.spring.cloud.appconfiguration.config.ConfigurationClientCustomizer; @@ -63,7 +66,8 @@ public class AppConfigurationReplicaClientsBuilder implements EnvironmentAware { /** * Invalid Formatted Connection String Error message */ - public static final String ENDPOINT_ERR_MSG = String.format("Connection string does not follow format %s.", CONN_STRING_REGEXP); + public static final String ENDPOINT_ERR_MSG = String.format("Connection string does not follow format %s.", + CONN_STRING_REGEXP); private static final Pattern CONN_STRING_PATTERN = Pattern.compile(CONN_STRING_REGEXP); @@ -81,7 +85,8 @@ public class AppConfigurationReplicaClientsBuilder implements EnvironmentAware { private final int defaultMaxRetries; - public AppConfigurationReplicaClientsBuilder(int defaultMaxRetries, ConfigurationClientBuilderFactory clientFactory, boolean credentialConfigured) { + public AppConfigurationReplicaClientsBuilder(int defaultMaxRetries, ConfigurationClientBuilderFactory clientFactory, + boolean credentialConfigured) { this.defaultMaxRetries = defaultMaxRetries; this.clientFactory = clientFactory; this.credentialConfigured = credentialConfigured; @@ -134,13 +139,16 @@ List buildClients(ConfigStore configStore) { int hasMultiConnectionString = configStore.getConnectionStrings().size() > 0 ? 1 : 0; if (hasSingleConnectionString + hasMultiEndpoints + hasMultiConnectionString > 1) { - throw new IllegalArgumentException("More than 1 Connection method was set for connecting to App Configuration."); + throw new IllegalArgumentException( + "More than 1 connection method was set for connecting to App Configuration."); } - boolean connectionStringIsPresent = configStore.getConnectionString() != null || configStore.getConnectionStrings().size() > 0; + boolean connectionStringIsPresent = configStore.getConnectionString() != null + || configStore.getConnectionStrings().size() > 0; if (credentialConfigured && connectionStringIsPresent) { - throw new IllegalArgumentException("More than 1 Connection method was set for connecting to App Configuration."); + throw new IllegalArgumentException( + "More than 1 connection method was set for connecting to App Configuration."); } List connectionStrings = configStore.getConnectionStrings(); @@ -160,7 +168,7 @@ List buildClients(ConfigStore configStore) { String endpoint = getEndpointFromConnectionString(connectionString); LOGGER.debug("Connecting to " + endpoint + " using Connecting String."); ConfigurationClientBuilder builder = createBuilderInstance().connectionString(connectionString); - + clients.add(modifyAndBuildClient(builder, endpoint, connectionStrings.size() - 1)); } } else { @@ -169,9 +177,8 @@ List buildClients(ConfigStore configStore) { if (!credentialConfigured) { // System Assigned Identity. Needs to be checked last as all of the above should // have an Endpoint. - LOGGER.debug("Connecting to {} using Azure System Assigned Identity or Azure User Assigned Identity.", endpoint); - ManagedIdentityCredentialBuilder micBuilder = new ManagedIdentityCredentialBuilder(); - builder.credential(micBuilder.build()); + LOGGER.debug("Connecting to {} using Azure System Assigned Identity.", endpoint); + builder.credential(new ManagedIdentityCredentialBuilder().build()); } builder.endpoint(endpoint); @@ -182,8 +189,35 @@ List buildClients(ConfigStore configStore) { return clients; } - private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBuilder builder, String endpoint, Integer replicaCount) { - TracingInfo tracingInfo = new TracingInfo(isDev, isKeyVaultConfigured, replicaCount, Configuration.getGlobalConfiguration()); + public AppConfigurationReplicaClient buildClient(String failoverEndpoint, ConfigStore configStore) { + + if (StringUtils.hasText(configStore.getConnectionString())) { + ConnectionString connectionString = new ConnectionString(configStore.getConnectionString()); + connectionString.setUri(failoverEndpoint); + ConfigurationClientBuilder builder = createBuilderInstance().connectionString(connectionString.toString()); + return modifyAndBuildClient(builder, failoverEndpoint, 0); + } else if (configStore.getConnectionStrings().size() > 0) { + ConnectionString connectionString = new ConnectionString(configStore.getConnectionStrings().get(0)); + connectionString.setUri(failoverEndpoint); + ConfigurationClientBuilder builder = createBuilderInstance().connectionString(connectionString.toString()); + return modifyAndBuildClient(builder, failoverEndpoint, 0); + } else { + ConfigurationClientBuilder builder = createBuilderInstance(); + if (!credentialConfigured) { + // System Assigned Identity. Needs to be checked last as all of the above should + // have an Endpoint. + LOGGER.debug("Connecting to {} using Azure System Assigned Identity.", failoverEndpoint); + builder.credential(new ManagedIdentityCredentialBuilder().build()); + } + builder.endpoint(failoverEndpoint); + return modifyAndBuildClient(builder, failoverEndpoint, 0); + } + } + + private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBuilder builder, String endpoint, + Integer replicaCount) { + TracingInfo tracingInfo = new TracingInfo(isDev, isKeyVaultConfigured, replicaCount, + Configuration.getGlobalConfiguration()); builder.addPolicy(new BaseAppConfigurationPolicy(tracingInfo)); if (clientProvider != null) { @@ -213,10 +247,13 @@ protected ConfigurationClientBuilder createBuilderInstance() { Function checkPropertyInt = parameter -> (Integer.parseInt(parameter)); Function checkPropertyDuration = parameter -> (DurationStyle.detectAndParse(parameter)); - int retries = checkProperty(MAX_RETRIES_PROPERTY_NAME, defaultMaxRetries, " isn't a valid integer, using default value.", checkPropertyInt); + int retries = checkProperty(MAX_RETRIES_PROPERTY_NAME, defaultMaxRetries, + " isn't a valid integer, using default value.", checkPropertyInt); - Duration baseDelay = checkProperty(BASE_DELAY_PROPERTY_NAME, DEFAULT_MIN_RETRY_POLICY, " isn't a valid Duration, using default value.", checkPropertyDuration); - Duration maxDelay = checkProperty(MAX_DELAY_PROPERTY_NAME, DEFAULT_MAX_RETRY_POLICY, " isn't a valid Duration, using default value.", checkPropertyDuration); + Duration baseDelay = checkProperty(BASE_DELAY_PROPERTY_NAME, DEFAULT_MIN_RETRY_POLICY, + " isn't a valid Duration, using default value.", checkPropertyDuration); + Duration maxDelay = checkProperty(MAX_DELAY_PROPERTY_NAME, DEFAULT_MAX_RETRY_POLICY, + " isn't a valid Duration, using default value.", checkPropertyDuration); retryStatagy = new ExponentialBackoff(retries, baseDelay, maxDelay); } @@ -251,11 +288,12 @@ private T checkProperty(String propertyName, T defaultValue, String errMsg, return value; } - - private static class ConnectionStringConnector implements ServiceConnectionStringProvider { - + + private static class ConnectionStringConnector + implements ServiceConnectionStringProvider { + private final String connectionString; - + ConnectionStringConnector(String connectionString) { this.connectionString = connectionString; } @@ -270,4 +308,66 @@ public AppConfiguration getServiceType() { return null; } } + + private static class ConnectionString { + private static final String ENDPOINT = "Endpoint="; + + private static final String ID = "Id="; + + private static final String SECRET_PREFIX = "Secret="; + + private URL baseUri; + + private final String id; + + private final String secret; + + ConnectionString(String connectionString) { + if (CoreUtils.isNullOrEmpty(connectionString)) { + throw new IllegalArgumentException("'connectionString' cannot be null or empty."); + } + String[] args = connectionString.split(";"); + if (args.length < 3) { + throw new IllegalArgumentException("invalid connection string segment count"); + } + URL baseUri = null; + String id = null; + String secret = null; + for (String arg : args) { + String segment = arg.trim(); + if (ENDPOINT.regionMatches(true, 0, segment, 0, ENDPOINT.length())) { + try { + baseUri = new URL(segment.substring(ENDPOINT.length())); + } catch (MalformedURLException ex) { + throw new IllegalArgumentException(ex); + } + } else if (ID.regionMatches(true, 0, segment, 0, ID.length())) { + id = segment.substring(ID.length()); + } else if (SECRET_PREFIX.regionMatches(true, 0, segment, 0, SECRET_PREFIX.length())) { + secret = segment.substring(SECRET_PREFIX.length()); + } + } + this.baseUri = baseUri; + this.id = id; + this.secret = secret; + if (this.baseUri == null || CoreUtils.isNullOrEmpty(this.id) + || this.secret == null || this.secret.length() == 0) { + throw new IllegalArgumentException("Could not parse 'connectionString'." + + " Expected format: 'endpoint={endpoint};id={id};secret={secret}'. Actual:" + connectionString); + } + } + + protected ConnectionString setUri(String uri) { + try { + this.baseUri = new URL(uri); + } catch (MalformedURLException ex) { + throw new IllegalArgumentException(ex); + } + return this; + } + + public String toString() { + return String.format("%s%s;%s%s;%s%s", ENDPOINT, baseUri, ID, id, SECRET_PREFIX, secret); + } + } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java index e7cca0856e008..16c6d2d05fdc5 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java @@ -4,13 +4,16 @@ import java.time.Instant; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.azure.spring.cloud.appconfiguration.config.AppConfigurationStoreHealth; +import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore; @@ -27,6 +30,8 @@ public class ConnectionManager { // Used if multiple connection method is given. private List clients; + private Map autoFailoverClients; + private String currentReplica; private AppConfigurationStoreHealth health; @@ -35,17 +40,22 @@ public class ConnectionManager { private final ConfigStore configStore; + private final ReplicaLookUp replicaLookUp; + /** * Creates a set of connections to an app configuration store. * @param clientBuilder Builder for App Configuration Clients * @param configStore Connection info for the store */ - ConnectionManager(AppConfigurationReplicaClientsBuilder clientBuilder, ConfigStore configStore) { + ConnectionManager(AppConfigurationReplicaClientsBuilder clientBuilder, ConfigStore configStore, + ReplicaLookUp replicaLookUp) { this.clientBuilder = clientBuilder; this.configStore = configStore; this.originEndpoint = configStore.getEndpoint(); this.health = AppConfigurationStoreHealth.NOT_LOADED; this.currentReplica = configStore.getEndpoint(); + this.autoFailoverClients = new HashMap<>(); + this.replicaLookUp = replicaLookUp; } /** @@ -63,7 +73,7 @@ void setCurrentClient(String replicaEndpoint) { /** * @return the originEndpoint */ - String getOriginEndpoint() { + String getMainEndpoint() { return originEndpoint; } @@ -92,7 +102,9 @@ List getAvailableClients(Boolean useCurrent) { boolean foundCurrent = !useCurrent; if (clients.size() == 1) { - availableClients.add(clients.get(0)); + if (clients.get(0).getBackoffEndTime().isBefore(Instant.now())) { + availableClients.add(clients.get(0)); + } } else if (clients.size() > 0) { for (AppConfigurationReplicaClient replicaClient : clients) { if (replicaClient.getEndpoint().equals(currentReplica)) { @@ -103,18 +115,39 @@ List getAvailableClients(Boolean useCurrent) { availableClients.add(replicaClient); } } - if (availableClients.size() == 0) { - this.health = AppConfigurationStoreHealth.DOWN; - } else { - this.health = AppConfigurationStoreHealth.UP; + } + + if (availableClients.size() == 0) { + List autoFailoverEndpoints = replicaLookUp.getAutoFailoverEndpoints(configStore.getEndpoint()); + + if (autoFailoverEndpoints.size() > 0) { + for (String failoverEndpoint : autoFailoverEndpoints) { + AppConfigurationReplicaClient client = autoFailoverClients.get(failoverEndpoint); + if (client == null) { + client = clientBuilder.buildClient(failoverEndpoint, configStore); + autoFailoverClients.put(failoverEndpoint, client); + } + if (client.getBackoffEndTime().isBefore(Instant.now())) { + availableClients.add(client); + break; + } + } } } + if (clients.size() > 0 && availableClients.size() == 0) { + this.health = AppConfigurationStoreHealth.DOWN; + } else if (clients.size() > 0) { + this.health = AppConfigurationStoreHealth.UP; + } return availableClients; } List getAllEndpoints() { - return clients.stream().map(AppConfigurationReplicaClient::getEndpoint).collect(Collectors.toList()); + List endpoints = clients.stream().map(AppConfigurationReplicaClient::getEndpoint) + .collect(Collectors.toList()); + endpoints.addAll(replicaLookUp.getAutoFailoverEndpoints(configStore.getEndpoint())); + return endpoints; } /** @@ -127,9 +160,13 @@ void backoffClient(String endpoint) { int failedAttempt = client.getFailedAttempts(); long backoffTime = BackoffTimeCalculator.calculateBackoff(failedAttempt); client.updateBackoffEndTime(Instant.now().plusNanos(backoffTime)); - break; + return; } } + + int failedAttempt = autoFailoverClients.get(endpoint).getFailedAttempts(); + long backoffTime = BackoffTimeCalculator.calculateBackoff(failedAttempt); + autoFailoverClients.get(endpoint).updateBackoffEndTime(Instant.now().plusNanos(backoffTime)); } /** @@ -140,6 +177,8 @@ void backoffClient(String endpoint) { void updateSyncToken(String endpoint, String syncToken) { clients.stream().filter(client -> client.getEndpoint().equals(endpoint)).findFirst() .ifPresent(client -> client.updateSyncToken(syncToken)); + autoFailoverClients.values().stream().filter(client -> client.getEndpoint().equals(endpoint)).findFirst() + .ifPresent(client -> client.updateSyncToken(syncToken)); } AppConfigurationStoreMonitoring getMonitoring() { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/ReplicaLookUp.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/ReplicaLookUp.java new file mode 100644 index 0000000000000..452cd8d8c291a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/ReplicaLookUp.java @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.appconfiguration.config.implementation.autofailover; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Semaphore; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.InitialDirContext; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientsBuilder; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; + +@Component +public class ReplicaLookUp { + + private static final String ORIGIN_PREFIX = "dns:/_origin._tcp."; + + private static final String REPLICA_PREFIX_ALT = "dns:/_alt"; + + private static final String REPLICA_PREFIX_TCP = "._tcp."; + + private static final String SRC_RECORD = "SRV"; + + private static final String[] TRUSTED_DOMAIN_LABELS = { "azconfig", "appconfig" }; + + private static final Duration FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL = Duration.ofHours(1); + + private static final Duration MINIMAL_CLIENT_REFRESH_INTERVAL = Duration.ofSeconds(30); + + InitialDirContext context; + + private Map> records = new HashMap>(); + + private Map wait = new HashMap<>(); + + private final AppConfigurationProperties properties; + + private final Semaphore semaphore; + + public ReplicaLookUp(AppConfigurationProperties properties) throws NamingException { + this.properties = properties; + this.context = new InitialDirContext(); + this.semaphore = new Semaphore(1); + } + + @Async + public void updateAutoFailoverEndpoints() { + if (semaphore.tryAcquire()) { + for (ConfigStore configStore : properties.getStores()) { + if (!configStore.isEnabled()) { + continue; + } + String mainEndpoint = configStore.getEndpoint(); + + List providedEndpoints = new ArrayList<>(); + if (configStore.getConnectionStrings().size() > 0) { + providedEndpoints = configStore.getConnectionStrings().stream().map(connectionString -> { + return (AppConfigurationReplicaClientsBuilder + .getEndpointFromConnectionString(connectionString)); + }).toList(); + } else if (configStore.getEndpoints().size() > 0) { + providedEndpoints = configStore.getEndpoints(); + } else { + providedEndpoints = List.of(configStore.getEndpoint()); + } + + try { + List srvRecords = findAutoFailoverEndpoints(mainEndpoint, providedEndpoints); + + srvRecords.sort((SRVRecord a, SRVRecord b) -> a.compareTo(b)); + + records.put(mainEndpoint, srvRecords); + wait.put(mainEndpoint, Instant.now().plus(FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL)); + } catch (AppConfigurationReplicaException e) { + wait.put(mainEndpoint, Instant.now().plus(MINIMAL_CLIENT_REFRESH_INTERVAL)); + } + + } + semaphore.release(); + } + } + + public List getAutoFailoverEndpoints(String mainEndpoint) { + List endpointRecords = records.get(mainEndpoint); + if (endpointRecords == null) { + return List.of(); + } + return endpointRecords.stream().map(record -> record.getEndpoint()).toList(); + } + + private List findAutoFailoverEndpoints(String endpoint, List providedEndpoints) + throws AppConfigurationReplicaException { + List records = new ArrayList<>(); + String host = ""; + try { + URI uri = new URI(endpoint); + host = uri.getHost(); + } catch (URISyntaxException e) { + // If endpoint uri is invalid then it will fail during startup + return new ArrayList<>(); + } + + SRVRecord origin = getOriginRecord(host); + if (origin != null) { + List replicas = getReplicaRecords(origin); + String knownDomain = getKnownDomain(endpoint); + + if (!providedEndpoints.contains(origin.getEndpoint()) && validate(knownDomain, origin.getEndpoint())) { + records.add(origin); + } + replicas.stream().forEach(replica -> { + if (!providedEndpoints.contains(replica.getEndpoint()) + && validate(knownDomain, replica.getEndpoint())) { + records.add(replica); + } + }); + } + return records; + } + + private SRVRecord getOriginRecord(String url) throws AppConfigurationReplicaException { + Attribute attribute = requestRecord(ORIGIN_PREFIX + url); + if (attribute != null) { + return parseHosts(attribute).get(0); + } + return null; + } + + private List getReplicaRecords(SRVRecord origin) throws AppConfigurationReplicaException { + List replicas = new ArrayList<>(); + int i = 0; + while (true) { + Attribute attribute = requestRecord( + REPLICA_PREFIX_ALT + i + REPLICA_PREFIX_TCP + origin.getTarget()); + + if (attribute == null) { + break; + } + + replicas.addAll(parseHosts(attribute)); + i++; + } + return replicas; + } + + private Attribute requestRecord(String name) throws AppConfigurationReplicaException { + Instant retryTime = Instant.now().plusSeconds(30); + while (retryTime.isAfter(Instant.now())) { + try { + return context.getAttributes(name, new String[] { SRC_RECORD }).get(SRC_RECORD); + } catch (NameNotFoundException e) { + // Found Last Record, should be the case that no SRV Record exists. + return null; + } catch (NamingException e) { + // Will retry for up to 30 seconds + } + } + throw new AppConfigurationReplicaException(); + } + + private List parseHosts(Attribute attribute) { + List hosts = new ArrayList<>(); + try { + NamingEnumeration records = attribute.getAll(); + while (records.hasMore()) { + hosts.add(new SRVRecord(((String) records.next()).toString().split(" "))); + } + } catch (NamingException e) { + } + + return hosts; + } + + private boolean validate(String knownDomain, String endpoint) { + if (!StringUtils.hasText(endpoint)) { + return false; + } + + if (!StringUtils.hasText(knownDomain)) { + return false; + } + return endpoint.endsWith(knownDomain); + } + + private String getKnownDomain(String knownHost) { + for (String label : TRUSTED_DOMAIN_LABELS) { + int index = knownHost.toLowerCase().indexOf("." + label + "."); + if (index > 0) { + return knownHost.substring(index); + } + } + return ""; + } + + private class AppConfigurationReplicaException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 1L; + + } + +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/SRVRecord.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/SRVRecord.java new file mode 100644 index 0000000000000..9646b83527d99 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/SRVRecord.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.appconfiguration.config.implementation.autofailover; + +public class SRVRecord { + + private final int priority; + + private final int weight; + + private final int port; + + private final String target; + + private static final String PROTOCAL = "https://"; + + public SRVRecord(String[] record) { + this.priority = Integer.valueOf(record[0]); + this.weight = Integer.valueOf(record[1]); + this.port = Integer.valueOf(record[2]); + this.target = record[3].substring(0, record[3].length() - 1); + } + + public int getPriority() { + return priority; + } + + public int getWeight() { + return weight; + } + + public int getPort() { + return port; + } + + public String getTarget() { + return target; + } + + public String getEndpoint() { + return PROTOCAL + target; + } + + public int compareTo(SRVRecord record) { + if (priority > record.getPriority()) { + return 1; + } + if (record.getPriority() > priority) { + return -1; + } + + if (weight > record.getWeight()) { + return 1; + } + if (record.getWeight() > weight) { + return -1; + } + + return 0; + } +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java index b503d3080e7d3..4b30bd4f0416e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/config/AppConfigurationBootstrapConfiguration.java @@ -2,6 +2,8 @@ // Licensed under the MIT License. package com.azure.spring.cloud.appconfiguration.config.implementation.config; +import javax.naming.NamingException; + import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -14,6 +16,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.util.StringUtils; import com.azure.data.appconfiguration.ConfigurationClientBuilder; @@ -24,6 +27,7 @@ import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationPropertySourceLocator; import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientFactory; import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientsBuilder; +import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProviderProperties; import com.azure.spring.cloud.autoconfigure.implementation.appconfiguration.AzureAppConfigurationProperties; @@ -47,6 +51,7 @@ @EnableConfigurationProperties({ AppConfigurationProperties.class, AppConfigurationProviderProperties.class }) @ConditionalOnClass(AppConfigurationPropertySourceLocator.class) @ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true) +@EnableAsync public class AppConfigurationBootstrapConfiguration { @Autowired @@ -55,11 +60,11 @@ public class AppConfigurationBootstrapConfiguration { @Bean AppConfigurationPropertySourceLocator sourceLocator(AppConfigurationProperties properties, AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, - AppConfigurationKeyVaultClientFactory keyVaultClientFactory) + AppConfigurationKeyVaultClientFactory keyVaultClientFactory, ReplicaLookUp replicaLookUp) throws IllegalArgumentException { return new AppConfigurationPropertySourceLocator(appProperties, clientFactory, keyVaultClientFactory, - properties.getRefreshInterval(), properties.getStores()); + properties.getRefreshInterval(), properties.getStores(), replicaLookUp); } @Bean @@ -101,8 +106,8 @@ AppConfigurationKeyVaultClientFactory appConfigurationKeyVaultClientFactory(Envi @Bean @ConditionalOnMissingBean AppConfigurationReplicaClientFactory buildClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder, - AppConfigurationProperties properties) { - return new AppConfigurationReplicaClientFactory(clientBuilder, properties.getStores()); + AppConfigurationProperties properties, ReplicaLookUp replicaLookUp) { + return new AppConfigurationReplicaClientFactory(clientBuilder, properties.getStores(), replicaLookUp); } /** @@ -150,6 +155,11 @@ AppConfigurationReplicaClientsBuilder replicaClientBuilder(Environment environme return clientBuilder; } + + @Bean + ReplicaLookUp replicaLookUp(AppConfigurationProperties properties) throws NamingException { + return new ReplicaLookUp(properties); + } private boolean isCredentialConfigured(AbstractAzureHttpConfigurationProperties properties) { if (properties.getCredential() != null) { diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java index 17ac625ba1558..878db9e464c80 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/properties/ConfigStore.java @@ -42,6 +42,8 @@ public final class ConfigStore { private List trimKeyPrefix; + private boolean replicaDiscoveryEnabled = true; + /** * @return the endpoint */ @@ -183,13 +185,26 @@ public List getTrimKeyPrefix() { } /** - * @param trimKeyPrefix the values to be trimmed from key names before being set to - * `@ConfigurationProperties` + * @param trimKeyPrefix the values to be trimmed from key names before being set to `@ConfigurationProperties` */ public void setTrimKeyPrefix(List trimKeyPrefix) { this.trimKeyPrefix = trimKeyPrefix; } + /** + * @return the replicaDiscoveryEnabled + */ + public boolean isReplicaDiscoveryEnabled() { + return replicaDiscoveryEnabled; + } + + /** + * @param replicaDiscoveryEnabled the replicaDiscoveryEnabled to set + */ + public void setReplicaDiscoveryEnabled(boolean replicaDiscoveryEnabled) { + this.replicaDiscoveryEnabled = replicaDiscoveryEnabled; + } + /** * @throws IllegalStateException Connection String URL endpoint is invalid */ diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocatorTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocatorTest.java index e5860131de862..bef246e7d1251 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocatorTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocatorTest.java @@ -45,6 +45,7 @@ import com.azure.data.appconfiguration.ConfigurationAsyncClient; import com.azure.data.appconfiguration.models.ConfigurationSetting; import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; +import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.Feature; import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector; @@ -118,6 +119,9 @@ public class AppConfigurationPropertySourceLocatorTest { @Mock private List watchKeyListMock; + + @Mock + private ReplicaLookUp replicaLookUpMock; private AppConfigurationPropertySourceLocator locator; @@ -204,7 +208,7 @@ public void compositeSourceIsCreated() { when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, - keyVaultClientFactory, null, stores); + keyVaultClientFactory, null, stores, replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -242,7 +246,7 @@ public void compositeSourceIsCreatedWithMonitoring() { .thenReturn(TestUtils.createItem("", watchKey, watchValue, watchLabel, "")); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -280,7 +284,7 @@ public void compositeSourceIsCreatedWithMonitoringNoWatchKey() { .thenReturn(null); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -304,7 +308,7 @@ public void devSourceIsCreated() { when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -326,7 +330,7 @@ public void multiSourceIsCreated() { when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -358,7 +362,7 @@ public void storeCreatedWithFeatureFlags() { when(replicaClientMock.listSettings(Mockito.any())).thenReturn(featureList); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -395,7 +399,7 @@ public void storeCreatedWithFeatureFlagsRequireAll() { when(replicaClientMock.getTracingInfo()).thenReturn(new TracingInfo(false, false, 0, null)); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -438,7 +442,7 @@ public void storeCreatedWithFeatureFlagsWithMonitoring() { when(replicaClientMock.listSettings(Mockito.any())).thenReturn(featureList); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -464,7 +468,7 @@ public void watchedKeyCheck() { when(configStoreMock.getFeatureFlags()).thenReturn(featureFlagStoreMock); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -498,7 +502,7 @@ public void defaultFailFastThrowException() { when(configStoreMock.getMonitoring()).thenReturn(monitor); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); when(clientFactoryMock.getAvailableClients(Mockito.anyString())).thenReturn(Arrays.asList(replicaClientMock)); when(replicaClientMock.getWatchKey(Mockito.any(), Mockito.anyString())).thenThrow(new RuntimeException()); @@ -513,7 +517,7 @@ public void refreshThrowException() throws IllegalArgumentException { AppConfigurationPropertySourceLocator.STARTUP.set(false); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); when(clientFactoryMock.getAvailableClients(Mockito.anyString())).thenReturn(Arrays.asList(replicaClientMock)); when(replicaClientMock.getWatchKey(Mockito.any(), Mockito.anyString())).thenThrow(new RuntimeException()); @@ -531,7 +535,7 @@ public void refreshThrowException() throws IllegalArgumentException { public void notFailFastShouldPass() { when(configStoreMock.isFailFast()).thenReturn(false); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); when(configStoreMock.isFailFast()).thenReturn(false); when(configStoreMock.getEndpoint()).thenReturn(TEST_STORE_NAME); @@ -555,7 +559,7 @@ public void multiplePropertySourcesExistForMultiStores() { TestUtils.addStore(properties, TEST_STORE_NAME_2, TEST_CONN_STRING_2, KEY_FILTER); locator = new AppConfigurationPropertySourceLocator(appProperties, - clientFactoryMock, keyVaultClientFactory, null, properties.getStores()); + clientFactoryMock, keyVaultClientFactory, null, properties.getStores(), replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); @@ -609,7 +613,7 @@ public Object getProperty(String name) { when(appPropertiesMock.getStartDate()).thenReturn(Instant.now()); locator = new AppConfigurationPropertySourceLocator(appPropertiesMock, clientFactoryMock, keyVaultClientFactory, - null, configStores); + null, configStores, replicaLookUpMock); assertThrows(RuntimeException.class, () -> locator.locate(env)); verify(appPropertiesMock, times(1)).getPrekillTime(); @@ -620,7 +624,7 @@ public void storeDisabled() { when(configStoreMock.isEnabled()).thenReturn(false); locator = new AppConfigurationPropertySourceLocator(appProperties, clientFactoryMock, keyVaultClientFactory, - null, stores); + null, stores, replicaLookUpMock); try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.updateState(Mockito.any())).thenReturn(null); PropertySource source = locator.locate(emptyEnvironment); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefreshTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefreshTest.java index b808aa6a07ce2..da09c05cdff58 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefreshTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefreshTest.java @@ -18,6 +18,7 @@ import org.springframework.context.ApplicationEventPublisher; import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationRefreshUtil.RefreshEventData; +import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import net.jcip.annotations.NotThreadSafe; @@ -26,6 +27,9 @@ public class AppConfigurationPullRefreshTest { @Mock private ApplicationEventPublisher publisher; + + @Mock + private ReplicaLookUp replicaLookUpMock; private final Duration refreshInterval = Duration.ofMinutes(10); @@ -51,11 +55,11 @@ public void refreshNoChange() throws InterruptedException, ExecutionException { .mockStatic(AppConfigurationRefreshUtil.class)) { refreshUtils .when(() -> AppConfigurationRefreshUtil.refreshStoresCheck(Mockito.eq(clientFactoryMock), - Mockito.eq(refreshInterval), Mockito.any(), Mockito.any())) + Mockito.eq(refreshInterval), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(eventData); AppConfigurationPullRefresh refresh = new AppConfigurationPullRefresh(clientFactoryMock, refreshInterval, - (long) 0); + (long) 0, replicaLookUpMock); assertFalse(refresh.refreshConfigurations().block()); } } @@ -67,11 +71,11 @@ public void refreshUpdate() throws InterruptedException, ExecutionException { .mockStatic(AppConfigurationRefreshUtil.class)) { refreshUtils .when(() -> AppConfigurationRefreshUtil.refreshStoresCheck(Mockito.eq(clientFactoryMock), - Mockito.eq(refreshInterval), Mockito.any(), Mockito.any())) + Mockito.eq(refreshInterval), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(eventData); AppConfigurationPullRefresh refresh = new AppConfigurationPullRefresh(clientFactoryMock, refreshInterval, - (long) 0); + (long) 0, replicaLookUpMock); refresh.setApplicationEventPublisher(publisher); assertTrue(refresh.refreshConfigurations().block()); } 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 3d00071b962ce..cf345388adfcb 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 @@ -31,6 +31,7 @@ import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting; 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.properties.AppConfigurationStoreMonitoring; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector; @@ -57,6 +58,9 @@ public class AppConfigurationRefreshUtilTest { @Mock private ConnectionManager connectionManagerMock; + + @Mock + private ReplicaLookUp replicaLookUpMock; private ConfigStore configStore; @@ -319,7 +323,7 @@ public void refreshStoresCheckSettingsTestNotEnabled(TestInfo testInfo) { // Monitor is disabled eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), - new ArrayList<>(), (long) 60); + new ArrayList<>(), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -349,7 +353,7 @@ public void refreshStoresCheckSettingsTestNotLoaded(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), - new ArrayList<>(), (long) 60); + new ArrayList<>(), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -380,7 +384,7 @@ public void refreshStoresCheckSettingsTestNotRefreshTime(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getState(endpoint)).thenReturn(newState); eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), - new ArrayList<>(), (long) 60); + new ArrayList<>(), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -413,7 +417,7 @@ public void refreshStoresCheckSettingsTestFailedRequest(TestInfo testInfo) { stateHolderMock.when(() -> StateHolder.getCurrentState()).thenReturn(updatedStateHolder); eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), - new ArrayList<>(), (long) 60); + new ArrayList<>(), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -449,7 +453,7 @@ public void refreshStoresCheckSettingsTestRefreshTimeNoChange(TestInfo testInfo) stateHolderMock.when(() -> StateHolder.getCurrentState()).thenReturn(updatedStateHolder); eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), - new ArrayList<>(), (long) 60); + new ArrayList<>(), (long) 60, replicaLookUpMock); assertEquals(newState, StateHolder.getState(endpoint)); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); @@ -487,7 +491,7 @@ public void refreshStoresCheckSettingsTestTriggerRefresh(TestInfo testInfo) { stateHolderMock.when(StateHolder::getCurrentState).thenReturn(currentStateMock); eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), - new ArrayList<>(), (long) 60); + new ArrayList<>(), (long) 60, replicaLookUpMock); assertTrue(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(1)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -521,7 +525,7 @@ public void refreshStoresCheckFeatureFlagTestNotLoaded(TestInfo testInfo) { // Monitor is disabled eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), - new ArrayList<>(), (long) 60); + new ArrayList<>(), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -554,7 +558,7 @@ public void refreshStoresCheckFeatureFlagTestNotRefreshTime(TestInfo testInfo) { // Monitor is disabled eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), - new ArrayList<>(), (long) 60); + new ArrayList<>(), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -596,7 +600,7 @@ public void refreshStoresCheckFeatureFlagTestNoChange(TestInfo testInfo) { // Monitor is disabled eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), - new ArrayList<>(), (long) 60); + new ArrayList<>(), (long) 60, replicaLookUpMock); assertFalse(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -641,7 +645,7 @@ public void refreshStoresCheckFeatureFlagTestTriggerRefresh(TestInfo testInfo) { // Monitor is disabled eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, Duration.ofMinutes(10), - new ArrayList<>(), (long) 60); + new ArrayList<>(), (long) 60, replicaLookUpMock); assertTrue(eventData.getDoRefresh()); verify(clientFactoryMock, times(1)).setCurrentConfigStoreClient(Mockito.eq(endpoint), Mockito.eq(endpoint)); verify(clientOriginMock, times(0)).getWatchKey(Mockito.anyString(), Mockito.anyString()); @@ -654,7 +658,7 @@ public void minRefreshPeriodTest() { try (MockedStatic stateHolderMock = Mockito.mockStatic(StateHolder.class)) { stateHolderMock.when(() -> StateHolder.getNextForcedRefresh()).thenReturn(Instant.now().minusSeconds(600)); RefreshEventData eventData = AppConfigurationRefreshUtil.refreshStoresCheck(clientFactoryMock, - Duration.ofMinutes(1), new ArrayList(), (long) 0); + Duration.ofMinutes(1), new ArrayList(), (long) 0, replicaLookUpMock); assertTrue(eventData.getDoRefresh()); assertEquals("Minimum refresh period reached. Refreshing configurations.", eventData.getMessage()); } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java index f84b759d57bbd..84e2d1bbaf348 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientBuilderTest.java @@ -19,7 +19,6 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.Mockito; @@ -46,10 +45,10 @@ public class AppConfigurationReplicaClientBuilderTest { AppConfigurationReplicaClientsBuilder clientBuilder; private ConfigStore configStore; - + @Mock private ConfigurationClientBuilderFactory clientFactoryMock; - + @Mock private Environment envMock; @@ -94,9 +93,8 @@ public void buildClientFromConnectionStringTest() { clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); clientBuilder.setEnvironment(envMock); AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); - - when(builderMock.connectionString(Mockito.anyString())).thenReturn(builderMock); + when(builderMock.connectionString(Mockito.anyString())).thenReturn(builderMock); List clients = spy.buildClients(configStore); @@ -155,8 +153,7 @@ public void buildClientsFromMultipleEndpointsTest() { assertEquals(2, clients.size()); } - @Test - @Disabled // Waiting on Server Side Support for connection strings + // @Test public void buildClientsFromMultipleConnectionStringsTest() { configStore = new ConfigStore(); List connectionStrings = new ArrayList<>(); @@ -164,7 +161,7 @@ public void buildClientsFromMultipleConnectionStringsTest() { connectionStrings.add(TEST_CONN_STRING); connectionStrings.add(TEST_CONN_STRING_GEO); - // configStore.setConnectionStrings(connectionStrings); + configStore.setConnectionStrings(connectionStrings); configStore.validateAndInit(); @@ -199,7 +196,119 @@ public void endpointAndConnectionString() { String message = assertThrows(IllegalArgumentException.class, () -> clientBuilder.buildClients(configStore).get(0)).getMessage(); - assertEquals("More than 1 Connection method was set for connecting to App Configuration.", message); + assertEquals("More than 1 connection method was set for connecting to App Configuration.", message); + } + + @Test + public void buildClientTest() { + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); + AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); + + ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); + when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); + when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); + + AppConfigurationReplicaClient replicaClient = spy.buildClient("https://fake.test.config.io", configStore); + + assertNotNull(replicaClient); + assertTrue(replicaClient.getBackoffEndTime().isBefore(Instant.now().plusSeconds(1))); + assertEquals(TEST_ENDPOINT, replicaClient.getEndpoint()); + assertEquals(0, replicaClient.getFailedAttempts()); + } + + @Test + public void buildClientConnectionStringTest() { + configStore.setConnectionString(TEST_CONN_STRING); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); + AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); + + ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); + when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); + when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); + when(builderMock.connectionString(Mockito.anyString())).thenReturn(builderMock); + + AppConfigurationReplicaClient replicaClient = spy.buildClient("https://fake.test.config.io", configStore); + + assertNotNull(replicaClient); + assertTrue(replicaClient.getBackoffEndTime().isBefore(Instant.now().plusSeconds(1))); + assertEquals(TEST_ENDPOINT, replicaClient.getEndpoint()); + assertEquals(0, replicaClient.getFailedAttempts()); + } + + @Test + public void buildClientConnectionStringsTest() { + configStore.setConnectionStrings(List.of(TEST_CONN_STRING, TEST_CONN_STRING_GEO)); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); + AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); + + ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); + when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); + when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); + when(builderMock.connectionString(Mockito.anyString())).thenReturn(builderMock); + + AppConfigurationReplicaClient replicaClient = spy.buildClient("https://fake.test.config.io", configStore); + + assertNotNull(replicaClient); + assertTrue(replicaClient.getBackoffEndTime().isBefore(Instant.now().plusSeconds(1))); + assertEquals(TEST_ENDPOINT, replicaClient.getEndpoint()); + assertEquals(0, replicaClient.getFailedAttempts()); + } + + @Test + public void buildClientConnectionStringInvalidTest() { + configStore.setConnectionString(TEST_CONN_STRING); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); + AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); + + ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); + when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); + when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); + when(builderMock.connectionString(Mockito.anyString())).thenReturn(builderMock); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> spy.buildClient("fake.test.config.io", configStore)); + + assertEquals("java.net.MalformedURLException: no protocol: fake.test.config.io", exception.getMessage()); + } + + @Test + public void buildClientConnectionStringInvalid2Test() { + configStore.setConnectionString("Not A Connection String"); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); + AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); + + ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); + when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); + when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); + when(builderMock.connectionString(Mockito.anyString())).thenReturn(builderMock); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> spy.buildClient("fake.test.config.io", configStore)); + + assertEquals("invalid connection string segment count", exception.getMessage()); + } + + @Test + public void buildClientConnectionStringInvalid3Test() { + configStore.setConnectionString("Not;A;Connection String"); + clientBuilder = new AppConfigurationReplicaClientsBuilder(0, clientFactoryMock, false); + clientBuilder.setEnvironment(envMock); + AppConfigurationReplicaClientsBuilder spy = Mockito.spy(clientBuilder); + + ConfigurationClientBuilder builder = new ConfigurationClientBuilder(); + when(builderMock.endpoint(Mockito.eq(TEST_ENDPOINT))).thenReturn(builder); + when(builderMock.addPolicy(Mockito.any())).thenReturn(builderMock); + when(builderMock.connectionString(Mockito.anyString())).thenReturn(builderMock); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> spy.buildClient("fake.test.config.io", configStore)); + + assertEquals("Could not parse 'connectionString'. Expected format: 'endpoint={endpoint};id={id};secret={secret}'. Actual:Not;A;Connection String", exception.getMessage()); } } diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactoryTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactoryTest.java index ad44817e0933f..ca373e6f6b5cd 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactoryTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactoryTest.java @@ -14,6 +14,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; public class AppConfigurationReplicaClientFactoryTest { @@ -22,6 +23,9 @@ public class AppConfigurationReplicaClientFactoryTest { @Mock private AppConfigurationReplicaClientsBuilder clientBuilderMock; + + @Mock + private ReplicaLookUp replicaLookUpMock; private final String originEndpoint = "clientFactoryTest.azconfig.io"; @@ -50,7 +54,7 @@ public void setup() { storeNoReplica.setEndpoint(noReplicaEndpoint); stores.add(storeNoReplica); - clientFactory = new AppConfigurationReplicaClientFactory(clientBuilderMock, stores); + clientFactory = new AppConfigurationReplicaClientFactory(clientBuilderMock, stores, replicaLookUpMock); } @Test diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManagerTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManagerTest.java index a39b6faaffd56..a20a90bc5d06e 100644 --- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManagerTest.java +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManagerTest.java @@ -20,6 +20,7 @@ import org.mockito.MockitoAnnotations; import com.azure.spring.cloud.appconfiguration.config.AppConfigurationStoreHealth; +import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp; import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; public class ConnectionManagerTest { @@ -32,6 +33,9 @@ public class ConnectionManagerTest { @Mock private AppConfigurationReplicaClient replicaClient2; + + @Mock + private ReplicaLookUp replicaLookUpMock; private ConnectionManager connectionManager; @@ -51,9 +55,9 @@ public void setup() { @Test public void getStoreIdentifierTest() { - connectionManager = new ConnectionManager(clientBuilderMock, configStore); + connectionManager = new ConnectionManager(clientBuilderMock, configStore, replicaLookUpMock); - assertEquals(TEST_ENDPOINT, connectionManager.getOriginEndpoint()); + assertEquals(TEST_ENDPOINT, connectionManager.getMainEndpoint()); configStore.setEndpoint(null); @@ -64,9 +68,9 @@ public void getStoreIdentifierTest() { configStore.setEndpoints(endpoints); configStore.validateAndInit(); - connectionManager = new ConnectionManager(clientBuilderMock, configStore); + connectionManager = new ConnectionManager(clientBuilderMock, configStore, replicaLookUpMock); - assertEquals("first.endpoint", connectionManager.getOriginEndpoint()); + assertEquals("first.endpoint", connectionManager.getMainEndpoint()); } @Test @@ -81,7 +85,7 @@ public void backoffTest() { configStore.validateAndInit(); - connectionManager = new ConnectionManager(clientBuilderMock, configStore); + connectionManager = new ConnectionManager(clientBuilderMock, configStore, replicaLookUpMock); List clients = new ArrayList<>(); clients.add(replicaClient1); @@ -138,9 +142,10 @@ public void backoffTest() { @Test public void updateSyncTokenTest() { String fakeToken = "fakeToken"; - ConnectionManager manager = new ConnectionManager(clientBuilderMock, configStore); + ConnectionManager manager = new ConnectionManager(clientBuilderMock, configStore, replicaLookUpMock); List clients = new ArrayList<>(); + when(replicaClient1.getBackoffEndTime()).thenReturn(Instant.now().minusSeconds(1)); clients.add(replicaClient1); when(clientBuilderMock.buildClients(Mockito.eq(configStore))).thenReturn(clients); @@ -156,7 +161,7 @@ public void updateSyncTokenTest() { @Test public void getAvailableClientsNotLoadedTest() { - ConnectionManager manager = new ConnectionManager(clientBuilderMock, configStore); + ConnectionManager manager = new ConnectionManager(clientBuilderMock, configStore, replicaLookUpMock); assertEquals(0, manager.getAvailableClients().size()); assertEquals(AppConfigurationStoreHealth.NOT_LOADED, manager.getHealth()); diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/ReplicaLookUpTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/ReplicaLookUpTest.java new file mode 100644 index 0000000000000..82c8642de5a75 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/ReplicaLookUpTest.java @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.appconfiguration.config.implementation.autofailover; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import java.util.List; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.InitialDirContext; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties; +import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore; + +public class ReplicaLookUpTest { + + private ReplicaLookUp replicaLookUp; + + private AppConfigurationProperties properties; + + @Mock + private InitialDirContext contextMock; + + @Mock + private Attributes srvOriginMock; + + @Mock + private Attributes srvReplicaMock; + + @Mock + private Attribute srvAttrOriginMock; + + @Mock + private Attribute srvAttrReplicaMock; + + @Mock + private NamingEnumeration namingOriginMock; + + @Mock + private NamingEnumeration namingReplicaMock; + + private ConfigStore configStore; + + @BeforeEach + public void init() { + MockitoAnnotations.openMocks(this); + + properties = new AppConfigurationProperties(); + configStore = new ConfigStore(); + configStore.setEndpoint("https://fake.endpoint.azconfig.test"); + } + + @Test + public void updateAutoFailoverEndpointsUnconfiguredTest() throws NamingException { + properties.setStores(List.of()); + replicaLookUp = new ReplicaLookUp(properties); + assertEquals(0, replicaLookUp.getAutoFailoverEndpoints("test.endpoint.azconfig.test").size()); + replicaLookUp.context = contextMock; + + replicaLookUp.updateAutoFailoverEndpoints(); + assertEquals(0, replicaLookUp.getAutoFailoverEndpoints("test.endpoint.azconfig.test").size()); + } + + @Test + public void updateAutoFailoverEndpointsNoReplicasTest() throws NamingException { + when(contextMock.getAttributes(Mockito.anyString(), Mockito.any())).thenReturn(srvOriginMock) + .thenThrow(new NameNotFoundException()); + when(srvOriginMock.get(Mockito.anyString())).thenReturn(srvAttrOriginMock); + Mockito.doReturn(namingOriginMock).when(srvAttrOriginMock).getAll(); + when(namingOriginMock.hasMore()).thenReturn(true).thenReturn(false); + Mockito.doReturn("1 1 1 fake.endpoint.azconfig.test.").when(namingOriginMock).next(); + + ConfigStore configStore = new ConfigStore(); + configStore.setEndpoint("https://fake.endpoint.azconfig.test"); + properties.setStores(List.of(configStore)); + replicaLookUp = new ReplicaLookUp(properties); + assertEquals(0, replicaLookUp.getAutoFailoverEndpoints("https://fake.endpoint.azconfig.test").size()); + replicaLookUp.context = contextMock; + + replicaLookUp.updateAutoFailoverEndpoints(); + assertEquals(0, replicaLookUp.getAutoFailoverEndpoints("https://fake.endpoint.azconfig.test").size()); + } + + @Test + public void updateAutoFailoverEndpointsAReplicasTest() throws NamingException { + setupMock(); + when(namingReplicaMock.hasMore()).thenReturn(true).thenReturn(false); + Mockito.doReturn("1 1 1 fake.endpoint.replica.azconfig.test.").when(namingReplicaMock).next(); + + assertEquals(0, replicaLookUp.getAutoFailoverEndpoints("https://fake.endpoint.azconfig.test").size()); + + replicaLookUp.updateAutoFailoverEndpoints(); + assertEquals(1, replicaLookUp.getAutoFailoverEndpoints("https://fake.endpoint.azconfig.test").size()); + } + + @Test + public void updateAutoFailoverEndpointsMuplipleReplicasTest() throws NamingException { + setupMock(); + when(namingReplicaMock.hasMore()).thenReturn(true).thenReturn(true).thenReturn(false); + Mockito.doReturn("1 1 1 fake.endpoint.replica.azconfig.test.") + .doReturn("1 1 1 fake.endpoint.replica2.azconfig.test.").when(namingReplicaMock).next(); + + assertEquals(0, replicaLookUp.getAutoFailoverEndpoints("https://fake.endpoint.azconfig.test").size()); + + replicaLookUp.updateAutoFailoverEndpoints(); + assertEquals(2, replicaLookUp.getAutoFailoverEndpoints("https://fake.endpoint.azconfig.test").size()); + } + + @Test + public void updateAutoFailoverEndpointsConnectionStringTest() throws NamingException { + configStore.setConnectionStrings(List.of("Endpoint=https://fake.endpoint.azconfig.test;Id=0;Secret=0=")); + setupMock(); + when(namingReplicaMock.hasMore()).thenReturn(true).thenReturn(false); + Mockito.doReturn("1 1 1 fake.endpoint.replica.azconfig.test.").when(namingReplicaMock).next(); + + assertEquals(0, replicaLookUp.getAutoFailoverEndpoints("https://fake.endpoint.azconfig.test").size()); + + replicaLookUp.updateAutoFailoverEndpoints(); + assertEquals(1, replicaLookUp.getAutoFailoverEndpoints("https://fake.endpoint.azconfig.test").size()); + } + + @Test + public void updateAutoFailoverEndpointsInvalidReplicaTest() throws NamingException { + setupMock(); + when(namingReplicaMock.hasMore()).thenReturn(true).thenReturn(false); + Mockito.doReturn("1 1 1 fake.endpoint.replica.invalid.test.").when(namingReplicaMock).next(); + + assertEquals(0, replicaLookUp.getAutoFailoverEndpoints("https://fake.endpoint.azconfig.test").size()); + + replicaLookUp.updateAutoFailoverEndpoints(); + assertEquals(0, replicaLookUp.getAutoFailoverEndpoints("https://fake.endpoint.azconfig.test").size()); + } + + private void setupMock() throws NamingException { + when(contextMock.getAttributes(Mockito.anyString(), Mockito.any())).thenReturn(srvOriginMock) + .thenReturn(srvReplicaMock).thenThrow(new NameNotFoundException()); + when(srvOriginMock.get(Mockito.anyString())).thenReturn(srvAttrOriginMock); + Mockito.doReturn(namingOriginMock).when(srvAttrOriginMock).getAll(); + when(namingOriginMock.hasMore()).thenReturn(true).thenReturn(false); + Mockito.doReturn("1 1 1 fake.endpoint.").when(namingOriginMock).next(); + + when(srvReplicaMock.get(Mockito.anyString())).thenReturn(srvAttrReplicaMock); + Mockito.doReturn(namingReplicaMock).when(srvAttrReplicaMock).getAll(); + + properties.setStores(List.of(configStore)); + replicaLookUp = new ReplicaLookUp(properties); + + replicaLookUp.context = contextMock; + } +} diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/SRVRecordTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/SRVRecordTest.java new file mode 100644 index 0000000000000..7c3401d6b7c91 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/test/java/com/azure/spring/cloud/appconfiguration/config/implementation/autofailover/SRVRecordTest.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.appconfiguration.config.implementation.autofailover; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class SRVRecordTest { + + @Test + public void createSRVRecord() { + String[] record = {"1", "2", "3", "fake-uri."}; + SRVRecord srvRecord = new SRVRecord(record); + assertEquals(1, srvRecord.getPriority()); + assertEquals(2, srvRecord.getWeight()); + assertEquals(3, srvRecord.getPort()); + assertEquals("fake-uri", srvRecord.getTarget()); + assertEquals("https://fake-uri", srvRecord.getEndpoint()); + } + + @Test + public void compareTest() { + String[] p1Record = {"1", "1", "1", "p1."}; + SRVRecord p1 = new SRVRecord(p1Record); + String[] p2Record = {"2", "2", "2", "p1."}; + SRVRecord p2 = new SRVRecord(p2Record); + assertEquals(-1, p1.compareTo(p2)); + assertEquals(1, p2.compareTo(p1)); + + String[] p1w2Record = {"1", "2", "1", "p1."}; + SRVRecord p1w2 = new SRVRecord(p1w2Record); + assertEquals(-1, p1.compareTo(p1w2)); + assertEquals(1, p1w2.compareTo(p1)); + + assertEquals(0, p1.compareTo(p1)); + } + +}