Adds support for retry with spring.config.import
Fixes gh-703
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
server:
|
||||
port: 0
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: testConsulConfigDataIntegrationTestApp
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.cloud.consul: DEBUG
|
||||
org.springframework.boot.context.config: TRACE
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user