Adds support for retry in profile specific properties.

in application-<profile>.{properties|yaml}

spring.config.import=configserver:http://configserver.example.com?fail-fast=true&max-attempts=10&max-interval=1500&multiplier=1.2&initial-interval=1100"

This required that the RetryTemplate be created as late as possible and it is no longer put in the bootstrap context. A RetryTemplateFactory was created so the LoaderInterceptor and the ConfigServerInstanceProvider can create a RetryTemplate. ConfigServerConfigDataLocationResolver creates RetryProperties from the url parameters if they are provided.

ConfigServerConfigDataLoader now creates a ConfigClientFailFastException and registers it in the application context rather and returns an empty ConfigData rather than throwing and exception. Then in ConfigClientAutoConfiguration an ApplicationStartedEvent is listened for and if the fail fast exception is there, throws it halting execution. This all allows logging in ConfigData to show.

Fixes gh-1797
This commit is contained in:
spencergibb
2021-05-26 11:31:03 -04:00
parent 529afcda4d
commit e9fa34032b
12 changed files with 276 additions and 35 deletions

View File

@@ -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:

View File

@@ -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<ApplicationStartedEvent> {
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
try {
ConfigClientFailFastException exception = event.getApplicationContext()
.getBean(ConfigClientFailFastException.class);
throw exception;
}
catch (NoSuchBeanDefinitionException e) {
// ignore
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
});
}

View File

@@ -87,7 +87,15 @@ public class ConfigServerConfigDataLoader implements ConfigDataLoader<ConfigServ
LoaderInterceptor interceptor = context.getBootstrapContext().get(LoaderInterceptor.class);
if (interceptor != null) {
Binder binder = context.getBootstrapContext().get(Binder.class);
return interceptor.apply(new LoadContext(context, resource, binder, this::doLoad));
try {
return interceptor.apply(new LoadContext(context, resource, binder, this::doLoad));
}
catch (ConfigClientFailFastException e) {
context.getBootstrapContext()
.addCloseListener(event -> 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<ConfigServ
else {
reason = "the resource is not optional";
}
throw new IllegalStateException("Could not locate PropertySource and " + reason + ", failing"
throw new ConfigClientFailFastException("Could not locate PropertySource and " + reason + ", failing"
+ (errorBody == null ? "" : ": " + errorBody), error);
}
logger.warn("Could not locate PropertySource (" + resource + "): "

View File

@@ -19,6 +19,7 @@ package org.springframework.cloud.config.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import org.apache.commons.logging.Log;
@@ -30,6 +31,7 @@ import org.springframework.boot.context.config.ConfigDataLocationResolver;
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
import org.springframework.boot.context.config.Profiles;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
@@ -61,7 +63,7 @@ public class ConfigServerConfigDataLocationResolver
return -1;
}
protected ConfigClientProperties loadProperties(ConfigDataLocationResolverContext context) {
protected PropertyHolder loadProperties(ConfigDataLocationResolverContext context, String uris) {
Binder binder = context.getBinder();
BindHandler bindHandler = getBindHandler(context);
ConfigClientProperties configClientProperties = binder
@@ -73,7 +75,47 @@ public class ConfigServerConfigDataLocationResolver
.orElse("application");
configClientProperties.setName(applicationName);
}
return configClientProperties;
PropertyHolder holder = new PropertyHolder();
holder.properties = configClientProperties;
// bind retry, override later
holder.retryProperties = binder.bind(RetryProperties.PREFIX, RetryProperties.class)
.orElseGet(RetryProperties::new);
if (StringUtils.hasText(uris)) {
String[] uri = StringUtils.commaDelimitedListToStringArray(uris);
String paramStr = null;
for (int i = 0; i < uri.length; i++) {
int paramIdx = uri[i].indexOf('?');
if (paramIdx > 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<ConfigServerConfigDataResource> 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<ServiceInstance> getConfigServerInstances(String serviceId) {
@@ -186,9 +229,17 @@ public class ConfigServerConfigDataLocationResolver
}
List<ConfigServerConfigDataResource> locations = new ArrayList<>();
locations.add(new ConfigServerConfigDataResource(properties, location.isOptional(), profiles));
locations.add(resource);
return locations;
}
private class PropertyHolder {
ConfigClientProperties properties;
RetryProperties retryProperties;
}
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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");
}

View File

@@ -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");
}
}

View File

@@ -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,

View File

@@ -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();
}
}
}