diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index a460b6ec..a6479e59 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -1565,9 +1565,21 @@ Then you need to add `spring-retry` and `spring-boot-starter-aop` to your classp The default behavior is to retry six times with an initial backoff interval of 1000ms and an exponential multiplier of 1.1 for subsequent backoffs. You can configure these properties (and others) by setting the `spring.cloud.config.retry.*` configuration properties. -TIP: To take full control of the retry behavior, add a `@Bean` of type `RetryOperationsInterceptor` with an ID of `configServerRetryInterceptor`. +TIP: To take full control of the retry behavior and are using legacy bootstrap, add a `@Bean` of type `RetryOperationsInterceptor` with an ID of `configServerRetryInterceptor`. Spring Retry has a `RetryInterceptorBuilder` that supports creating one. +=== Config Client Retry with spring.config.import + +Retry works with the Spring Boot `spring.config.import` statement and the normal properties work. However, if the import statement is in a profile, such as `application-prod.properties`, then you need a different way to configure retry. Configuration needs to be placed as url parameters on the import statement. + +.application-prod.properties +[source,properties] +---- +spring.config.import=configserver:http://configserver.example.com?fail-fast=true&max-attempts=10&max-interval=1500&multiplier=1.2&initial-interval=1100" +---- + +This sets `spring.cloud.config.fail-fast=true` (notice the missing prefix above) and all the available `spring.cloud.config.retry.*` configuration properties. + === Locating Remote Configuration Resources The Config Service serves property sources from `/{application}/{profile}/{label}`, where the default bindings in the client app are as follows: diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java index b1c9744a..c6f7e978 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java @@ -17,14 +17,17 @@ package org.springframework.cloud.config.client; import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; @@ -85,4 +88,21 @@ public class ConfigClientAutoConfiguration { } + @Configuration(proxyBeanMethods = false) + protected class ConfigClientFailFastListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationStartedEvent event) { + try { + ConfigClientFailFastException exception = event.getApplicationContext() + .getBean(ConfigClientFailFastException.class); + throw exception; + } + catch (NoSuchBeanDefinitionException e) { + // ignore + } + } + + } + } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientFailFastException.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientFailFastException.java new file mode 100644 index 00000000..33b861a9 --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientFailFastException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.client; + +public class ConfigClientFailFastException extends IllegalStateException { + + public ConfigClientFailFastException(String message, Exception error) { + super(message, error); + } + +} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRetryBootstrapper.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRetryBootstrapper.java index 9aaaa94b..71135273 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRetryBootstrapper.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRetryBootstrapper.java @@ -18,7 +18,6 @@ package org.springframework.cloud.config.client; import org.springframework.boot.BootstrapRegistry; import org.springframework.boot.BootstrapRegistryInitializer; -import org.springframework.boot.context.properties.bind.Binder; import org.springframework.cloud.config.client.ConfigServerBootstrapper.LoaderInterceptor; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.ClassUtils; @@ -40,23 +39,15 @@ public class ConfigClientRetryBootstrapper implements BootstrapRegistryInitializ return; } - registry.registerIfAbsent(RetryProperties.class, context -> context.get(Binder.class) - .bind(RetryProperties.PREFIX, RetryProperties.class).orElseGet(RetryProperties::new)); - - registry.registerIfAbsent(RetryTemplate.class, context -> { - RetryProperties properties = context.get(RetryProperties.class); - return RetryTemplate.builder().maxAttempts(properties.getMaxAttempts()).exponentialBackoff( - properties.getInitialInterval(), properties.getMultiplier(), properties.getMaxInterval()).build(); - }); - registry.registerIfAbsent(LoaderInterceptor.class, context -> { - Binder binder = context.get(Binder.class); - boolean failFast = binder.bind(ConfigClientProperties.PREFIX + ".fail-fast", Boolean.class).orElse(false); - if (failFast) { - RetryTemplate retryTemplate = context.get(RetryTemplate.class); - return loadContext -> retryTemplate.execute(retryContext -> loadContext.getInvocation() - .apply(loadContext.getLoaderContext(), loadContext.getResource())); + registry.registerIfAbsent(LoaderInterceptor.class, context -> loadContext -> { + ConfigServerConfigDataResource resource = loadContext.getResource(); + if (resource.getProperties().isFailFast()) { + RetryProperties properties = resource.getRetryProperties(); + RetryTemplate retryTemplate = RetryTemplateFactory.create(properties, resource.getLog()); + return retryTemplate.execute( + retryContext -> loadContext.getInvocation().apply(loadContext.getLoaderContext(), resource)); } - return null; + return loadContext.getInvocation().apply(loadContext.getLoaderContext(), resource); }); } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java index 0c2e48dc..7da09fdb 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLoader.java @@ -87,7 +87,15 @@ public class ConfigServerConfigDataLoader implements ConfigDataLoader event.getApplicationContext().getBeanFactory() + .registerSingleton(ConfigClientFailFastException.class.getSimpleName(), e)); + return new ConfigData(Collections.emptyList()); + } } } return doLoad(context, resource); @@ -182,7 +190,7 @@ public class ConfigServerConfigDataLoader implements ConfigDataLoader 0) { + if (i == 0) { + // only gather params from first uri + paramStr = uri[i].substring(paramIdx + 1); + } + uri[i] = uri[i].substring(0, paramIdx); + } + } + if (StringUtils.hasText(paramStr)) { + Properties properties = StringUtils + .splitArrayElementsIntoProperties(StringUtils.delimitedListToStringArray(paramStr, "&"), "="); + if (properties != null) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(() -> properties.getProperty("fail-fast")).as(Boolean::valueOf) + .to(configClientProperties::setFailFast); + map.from(() -> properties.getProperty("max-attempts")).as(Integer::valueOf) + .to(holder.retryProperties::setMaxAttempts); + map.from(() -> properties.getProperty("max-interval")).as(Long::valueOf) + .to(holder.retryProperties::setMaxInterval); + map.from(() -> properties.getProperty("multiplier")).as(Double::valueOf) + .to(holder.retryProperties::setMultiplier); + map.from(() -> properties.getProperty("initial-interval")).as(Long::valueOf) + .to(holder.retryProperties::setInitialInterval); + } + } + configClientProperties.setUri(uri); + } + + return holder; } private BindHandler getBindHandler(ConfigDataLocationResolverContext context) { @@ -112,13 +154,9 @@ public class ConfigServerConfigDataLocationResolver public List resolveProfileSpecific( ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles) throws ConfigDataLocationNotFoundException { - ConfigClientProperties properties = loadProperties(resolverContext); String uris = location.getNonPrefixedValue(getPrefix()); - - if (StringUtils.hasText(uris)) { - String[] uri = StringUtils.commaDelimitedListToStringArray(uris); - properties.setUri(uri); - } + PropertyHolder propertyHolder = loadProperties(resolverContext, uris); + ConfigClientProperties properties = propertyHolder.properties; ConfigurableBootstrapContext bootstrapContext = resolverContext.getBootstrapContext(); bootstrapContext.registerIfAbsent(ConfigClientProperties.class, InstanceSupplier.of(properties)); @@ -138,6 +176,11 @@ public class ConfigServerConfigDataLocationResolver return factory.create(); }); + ConfigServerConfigDataResource resource = new ConfigServerConfigDataResource(properties, location.isOptional(), + profiles); + resource.setLog(log); + resource.setRetryProperties(propertyHolder.retryProperties); + boolean discoveryEnabled = resolverContext.getBinder() .bind(CONFIG_DISCOVERY_ENABLED, Bindable.of(Boolean.class), getBindHandler(resolverContext)) .orElse(false); @@ -155,7 +198,7 @@ public class ConfigServerConfigDataLocationResolver ConfigServerInstanceProvider instanceProvider; if (ConfigClientRetryBootstrapper.RETRY_IS_PRESENT && retryEnabled) { log.debug(LogMessage.format("discovery plus retry enabled")); - RetryTemplate retryTemplate = context.get(RetryTemplate.class); + RetryTemplate retryTemplate = RetryTemplateFactory.create(propertyHolder.retryProperties, log); instanceProvider = new ConfigServerInstanceProvider(function) { @Override public List getConfigServerInstances(String serviceId) { @@ -186,9 +229,17 @@ public class ConfigServerConfigDataLocationResolver } List locations = new ArrayList<>(); - locations.add(new ConfigServerConfigDataResource(properties, location.isOptional(), profiles)); + locations.add(resource); return locations; } + private class PropertyHolder { + + ConfigClientProperties properties; + + RetryProperties retryProperties; + + } + } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataResource.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataResource.java index 23027197..b4413051 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataResource.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataResource.java @@ -19,6 +19,8 @@ package org.springframework.cloud.config.client; import java.util.List; import java.util.Objects; +import org.apache.commons.logging.Log; + import org.springframework.boot.context.config.ConfigDataResource; import org.springframework.boot.context.config.Profiles; import org.springframework.core.style.ToStringCreator; @@ -32,6 +34,10 @@ public class ConfigServerConfigDataResource extends ConfigDataResource { private final Profiles profiles; + private RetryProperties retryProperties; + + private Log log; + public ConfigServerConfigDataResource(ConfigClientProperties properties, boolean optional, Profiles profiles) { this.properties = properties; this.optional = optional; @@ -58,6 +64,22 @@ public class ConfigServerConfigDataResource extends ConfigDataResource { return this.profiles.getAccepted(); } + public void setLog(Log log) { + this.log = log; + } + + public Log getLog() { + return this.log; + } + + public RetryProperties getRetryProperties() { + return this.retryProperties; + } + + public void setRetryProperties(RetryProperties retryProperties) { + this.retryProperties = retryProperties; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/RetryTemplateFactory.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/RetryTemplateFactory.java new file mode 100644 index 00000000..5aa2abbc --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/RetryTemplateFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.client; + +import java.lang.reflect.Field; + +import org.apache.commons.logging.Log; + +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.ReflectionUtils; + +public final class RetryTemplateFactory { + + private static final Field field; + + static { + field = ReflectionUtils.findField(RetryTemplate.class, "logger"); + if (field != null) { + ReflectionUtils.makeAccessible(field); + } + } + + private RetryTemplateFactory() { + + } + + public static RetryTemplate create(RetryProperties properties, Log log) { + RetryTemplate retryTemplate = RetryTemplate.builder().maxAttempts(properties.getMaxAttempts()) + .exponentialBackoff(properties.getInitialInterval(), properties.getMultiplier(), + properties.getMaxInterval()) + .build(); + try { + field.set(retryTemplate, log); + } + catch (IllegalAccessException e) { + if (log.isErrorEnabled()) { + log.error("error setting retry log", e); + } + } + return retryTemplate; + } + +} diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolverTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolverTests.java index ad46317e..03cfe619 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolverTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolverTests.java @@ -108,6 +108,16 @@ public class ConfigServerConfigDataLocationResolverTests { assertThat(resource.getProperties().getName()).isEqualTo("myconfigname"); } + @Test + void retryPropertiesShouldBeDefaultByDefault() { + ConfigServerConfigDataResource resource = testResolveProvileSpecific(); + RetryProperties defaultRetry = new RetryProperties(); + assertThat(resource.getRetryProperties().getMaxAttempts()).isEqualTo(defaultRetry.getMaxAttempts()); + assertThat(resource.getRetryProperties().getMaxInterval()).isEqualTo(defaultRetry.getMaxInterval()); + assertThat(resource.getRetryProperties().getInitialInterval()).isEqualTo(defaultRetry.getInitialInterval()); + assertThat(resource.getRetryProperties().getMultiplier()).isEqualTo(defaultRetry.getMultiplier()); + } + private ConfigServerConfigDataResource testResolveProvileSpecific() { return testResolveProvileSpecific("default"); } diff --git a/spring-cloud-config-sample/src/test/java/sample/ApplicationFailFastTests.java b/spring-cloud-config-sample/src/test/java/sample/ApplicationFailFastTests.java index 6c45ac5d..8cebe0ed 100644 --- a/spring-cloud-config-sample/src/test/java/sample/ApplicationFailFastTests.java +++ b/spring-cloud-config-sample/src/test/java/sample/ApplicationFailFastTests.java @@ -16,12 +16,17 @@ package sample; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@ExtendWith(OutputCaptureExtension.class) public class ApplicationFailFastTests { @Test @@ -34,13 +39,15 @@ public class ApplicationFailFastTests { } @Test - public void configDataContextFails() { + public void configDataContextFails(CapturedOutput output) { assertThatThrownBy(() -> { new SpringApplicationBuilder().sources(Application.class).run("--server.port=0", "--spring.cloud.config.enabled=true", "--spring.cloud.config.fail-fast=true", - "--spring.config.import=optional:configserver:http://serverhostdoesnotexist:1234"); + "--spring.config.import=optional:configserver:http://serverhostdoesnotexist:1234", + "--spring.cloud.config.server.enabled=false", + "--logging.level.org.springframework.boot.context.config=TRACE"); }).as("Exception not caused by fail fast").hasMessageContaining("fail fast"); - + assertThat(output).contains("Retry: count=5"); } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java index 878d6478..1cf57550 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.cloud.config.server.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -26,6 +27,7 @@ import org.springframework.context.annotation.Import; */ @Configuration(proxyBeanMethods = false) @ConditionalOnBean(ConfigServerConfiguration.Marker.class) +@ConditionalOnProperty(name = ConfigServerProperties.PREFIX + ".enabled", matchIfMissing = true) @EnableConfigurationProperties(ConfigServerProperties.class) @Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class, ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class, diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerProperties.java index 90b5dc24..8e8b1fdc 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerProperties.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerProperties.java @@ -20,14 +20,25 @@ import java.util.LinkedHashMap; import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.style.ToStringCreator; /** * @author Dave Syer * @author Roy Clarkson */ -@ConfigurationProperties("spring.cloud.config.server") +@ConfigurationProperties(ConfigServerProperties.PREFIX) public class ConfigServerProperties { + /** + * Config Server properties prefix. + */ + public static final String PREFIX = "spring.cloud.config.server"; + + /** + * Flag indicating config server is enabled. + */ + private boolean enabled = true; + /** * Flag indicating that the config server should initialize its own Environment with * properties from the remote repository. Off by default because it delays startup but @@ -90,6 +101,14 @@ public class ConfigServerProperties { */ private Encrypt encrypt = new Encrypt(); + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public Encrypt getEncrypt() { return this.encrypt; } @@ -166,6 +185,16 @@ public class ConfigServerProperties { this.failOnCompositeError = failOnCompositeError; } + @Override + public String toString() { + return new ToStringCreator(this).append("enabled", enabled).append("bootstrap", bootstrap) + .append("prefix", prefix).append("defaultLabel", defaultLabel).append("overrides", overrides) + .append("stripDocumentFromYaml", stripDocumentFromYaml).append("acceptEmpty", acceptEmpty) + .append("defaultApplicationName", defaultApplicationName).append("defaultProfile", defaultProfile) + .append("failOnCompositeError", failOnCompositeError).append("encrypt", encrypt).toString(); + + } + /** * Encryption properties. */ @@ -198,6 +227,13 @@ public class ConfigServerProperties { this.plainTextEncrypt = plainTextEncrypt; } + @Override + public String toString() { + return new ToStringCreator(this).append("enabled", enabled).append("plainTextEncrypt", plainTextEncrypt) + .toString(); + + } + } }