Adds support for retry with spring.config.import

Fixes gh-703
This commit is contained in:
spencergibb
2021-03-15 23:19:43 -04:00
parent 77c0349de2
commit 6aae1854dd
10 changed files with 393 additions and 2 deletions

View File

@@ -16,15 +16,25 @@
package org.springframework.cloud.consul.config;
import java.util.function.BiFunction;
import java.util.function.Function;
import com.ecwid.consul.v1.ConsulClient;
import org.springframework.boot.BootstrapContext;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.Bootstrapper;
import org.springframework.boot.context.config.ConfigData;
import org.springframework.boot.context.config.ConfigDataLoaderContext;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.consul.ConsulProperties;
import org.springframework.util.Assert;
public abstract class ConsulBootstrapper {
public class ConsulBootstrapper implements Bootstrapper {
private Function<BootstrapContext, ConsulClient> consulClientFactory;
private LoaderInterceptor loaderInterceptor;
static Bootstrapper fromConsulProperties(Function<ConsulProperties, ConsulClient> factory) {
return registry -> registry.register(ConsulClient.class, context -> {
@@ -37,4 +47,79 @@ public abstract class ConsulBootstrapper {
return registry -> registry.register(ConsulClient.class, factory::apply);
}
static ConsulBootstrapper create() {
return new ConsulBootstrapper();
}
// TODO: document there will be a ConsulProperties in BootstrapContext
public ConsulBootstrapper withConsulClientFactory(Function<BootstrapContext, ConsulClient> consulClientFactory) {
this.consulClientFactory = consulClientFactory;
return this;
}
public ConsulBootstrapper withLoaderInterceptor(LoaderInterceptor loaderInterceptor) {
this.loaderInterceptor = loaderInterceptor;
return this;
}
@Override
public void intitialize(BootstrapRegistry registry) {
if (consulClientFactory != null) {
registry.register(ConsulClient.class, consulClientFactory::apply);
}
if (loaderInterceptor != null) {
registry.register(LoaderInterceptor.class, BootstrapRegistry.InstanceSupplier.of(loaderInterceptor));
}
}
public interface LoaderInterceptor extends Function<LoadContext, ConfigData> {
}
@FunctionalInterface
public interface LoaderInvocation
extends BiFunction<ConfigDataLoaderContext, ConsulConfigDataResource, ConfigData> {
}
public static class LoadContext {
private final ConfigDataLoaderContext loaderContext;
private final ConsulConfigDataResource resource;
private final Binder binder;
private final LoaderInvocation invocation;
LoadContext(ConfigDataLoaderContext loaderContext, ConsulConfigDataResource resource, Binder binder,
LoaderInvocation invocation) {
Assert.notNull(loaderContext, "loaderContext may not be null");
Assert.notNull(resource, "resource may not be null");
Assert.notNull(binder, "binder may not be null");
Assert.notNull(invocation, "invocation may not be null");
this.loaderContext = loaderContext;
this.resource = resource;
this.binder = binder;
this.invocation = invocation;
}
public ConfigDataLoaderContext getLoaderContext() {
return this.loaderContext;
}
public ConsulConfigDataResource getResource() {
return this.resource;
}
public Binder getBinder() {
return this.binder;
}
public LoaderInvocation getInvocation() {
return this.invocation;
}
}
}

View File

@@ -25,6 +25,9 @@ import org.springframework.boot.context.config.ConfigData;
import org.springframework.boot.context.config.ConfigDataLoader;
import org.springframework.boot.context.config.ConfigDataLoaderContext;
import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.consul.config.ConsulBootstrapper.LoadContext;
import org.springframework.cloud.consul.config.ConsulBootstrapper.LoaderInterceptor;
public class ConsulConfigDataLoader implements ConfigDataLoader<ConsulConfigDataResource> {
@@ -36,6 +39,17 @@ public class ConsulConfigDataLoader implements ConfigDataLoader<ConsulConfigData
@Override
public ConfigData load(ConfigDataLoaderContext context, ConsulConfigDataResource resource) {
if (context.getBootstrapContext().isRegistered(LoaderInterceptor.class)) {
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));
}
}
return doLoad(context, resource);
}
public ConfigData doLoad(ConfigDataLoaderContext context, ConsulConfigDataResource resource) {
try {
ConsulClient consul = getBean(context, ConsulClient.class);
ConsulConfigIndexes indexes = getBean(context, ConsulConfigIndexes.class);
@@ -48,6 +62,9 @@ public class ConsulConfigDataLoader implements ConfigDataLoader<ConsulConfigData
return new ConfigData(Collections.singletonList(propertySource));
}
catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Error getting properties from consul: " + resource, e);
}
throw new ConfigDataResourceNotFoundException(resource, e);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2013-2020 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.consul.config;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.Bootstrapper;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.consul.RetryProperties;
import org.springframework.cloud.consul.config.ConsulBootstrapper.LoaderInterceptor;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.ClassUtils;
/**
* Consul Retry Bootstrapper.
*
* @author Spencer Gibb
* @since 3.0.2
*/
public class ConsulRetryBootstrapper implements Bootstrapper {
static final boolean RETRY_IS_PRESENT = ClassUtils.isPresent("org.springframework.retry.annotation.Retryable",
null);
@Override
public void intitialize(BootstrapRegistry registry) {
if (!RETRY_IS_PRESENT) {
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);
if (properties.isEnabled()) {
return RetryTemplate.builder().maxAttempts(properties.getMaxAttempts())
.exponentialBackoff(properties.getInitialInterval(), properties.getMultiplier(),
properties.getMaxInterval())
.build();
}
return null;
});
registry.registerIfAbsent(LoaderInterceptor.class, context -> {
RetryTemplate retryTemplate = context.get(RetryTemplate.class);
if (retryTemplate != null) {
return loadContext -> retryTemplate.execute(retryContext -> loadContext.getInvocation()
.apply(loadContext.getLoaderContext(), loadContext.getResource()));
}
// disabled
return null;
});
}
}

View File

@@ -19,3 +19,7 @@ org.springframework.cloud.consul.config.ConsulConfigDataLocationResolver
# ConfigData Loaders
org.springframework.boot.context.config.ConfigDataLoader=\
org.springframework.cloud.consul.config.ConsulConfigDataLoader
# Spring Boot Bootstrappers
org.springframework.boot.Bootstrapper=\
org.springframework.cloud.consul.config.ConsulRetryBootstrapper

View File

@@ -22,9 +22,14 @@ import org.springframework.core.style.ToStringCreator;
/**
* @author Spencer Gibb
*/
@ConfigurationProperties("spring.cloud.consul.retry")
@ConfigurationProperties(RetryProperties.PREFIX)
public class RetryProperties {
/**
* Consul Retry Properties prefix.
*/
public static final String PREFIX = "spring.cloud.consul.retry";
/** If consul retry is enabled. */
private boolean enabled = true;

View File

@@ -17,5 +17,6 @@
<modules>
<module>spring-cloud-consul-bootstrap-tests</module>
<module>spring-cloud-consul-configdata-tests</module>
<module>spring-cloud-consul-configdata-retry-tests</module>
</modules>
</project>

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-consul-configdata-retry-tests</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Consul ConfigData Retry Tests</name>
<description>Spring Cloud Consul ConfigData Retry Tests</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-consul-integration-tests</artifactId>
<version>3.0.2-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<!--skip deploy -->
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- Only needed at compile time -->
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-consul-core</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2013-2020 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.consul.configdatatests;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Spencer Gibb
*/
@SpringBootApplication
public class ConsulConfigDataRetryApplication {
public static void main(String[] args) {
SpringApplication.run(ConsulConfigDataRetryApplication.class, args);
}
}

View File

@@ -0,0 +1,11 @@
server:
port: 0
spring:
application:
name: testConsulConfigDataIntegrationTestApp
logging:
level:
org.springframework.cloud.consul: DEBUG
org.springframework.boot.context.config: TRACE

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2013-2020 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.consul.configdatatests;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.consul.config.ConsulBootstrapper;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
@DirtiesContext
public class ConsulConfigDataRetryApplicationTests {
private static final String APP_NAME = "testConsulConfigDataRetryIntegration";
private static final String PREFIX = "_configDataRetryIntegrationTests_config__";
private static final String ROOT = PREFIX + UUID.randomUUID();
private static ConfigurableApplicationContext context;
private static final AtomicInteger count = new AtomicInteger();
@BeforeAll
public static void setup() {
context = new SpringApplicationBuilder(ConsulConfigDataRetryApplication.class).addBootstrapper(registry -> {
registry.register(ConsulBootstrapper.LoaderInterceptor.class, context -> {
RetryTemplate retryTemplate = context.get(RetryTemplate.class);
if (retryTemplate != null) {
return loadContext -> retryTemplate.execute(retryContext -> {
count.incrementAndGet();
return loadContext.getInvocation().apply(loadContext.getLoaderContext(),
loadContext.getResource());
});
}
// disabled
return null;
});
}).run("--spring.application.name=" + APP_NAME, "--spring.cloud.consul.retry.enabled=true",
"--spring.cloud.consul.retry.max-attempts=2",
// non-existent consul host and port
"--spring.config.import=optional:consul:somehost:1234", "--spring.cloud.consul.config.prefix=" + ROOT,
"--spring.cloud.consul.config.watch.delay=10");
}
@AfterAll
public static void teardown() {
if (context != null) {
context.close();
}
}
@Test
public void contextLoads() {
// four default contexts times two retries
assertThat(count.get()).as("Retry failed").isGreaterThanOrEqualTo(8);
}
}