diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulBootstrapper.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulBootstrapper.java index 58ea5a06..e95502c9 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulBootstrapper.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulBootstrapper.java @@ -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 consulClientFactory; + + private LoaderInterceptor loaderInterceptor; static Bootstrapper fromConsulProperties(Function 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 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 { + + } + + @FunctionalInterface + public interface LoaderInvocation + extends BiFunction { + + } + + 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; + } + + } + } diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLoader.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLoader.java index 5c118539..6a35437c 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLoader.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigDataLoader.java @@ -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 { @@ -36,6 +39,17 @@ public class ConsulConfigDataLoader implements ConfigDataLoader 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; + }); + + } + +} diff --git a/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories b/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories index 6c1c19a8..4a29c24c 100644 --- a/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-consul-config/src/main/resources/META-INF/spring.factories @@ -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 diff --git a/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/RetryProperties.java b/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/RetryProperties.java index 8ff78c0c..dbf1e7ad 100644 --- a/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/RetryProperties.java +++ b/spring-cloud-consul-core/src/main/java/org/springframework/cloud/consul/RetryProperties.java @@ -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; diff --git a/spring-cloud-consul-integration-tests/pom.xml b/spring-cloud-consul-integration-tests/pom.xml index d217c4c7..5fcfc8c4 100644 --- a/spring-cloud-consul-integration-tests/pom.xml +++ b/spring-cloud-consul-integration-tests/pom.xml @@ -17,5 +17,6 @@ spring-cloud-consul-bootstrap-tests spring-cloud-consul-configdata-tests + spring-cloud-consul-configdata-retry-tests diff --git a/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/pom.xml b/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/pom.xml new file mode 100644 index 00000000..e6aa200f --- /dev/null +++ b/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + spring-cloud-consul-configdata-retry-tests + jar + Spring Cloud Consul ConfigData Retry Tests + Spring Cloud Consul ConfigData Retry Tests + + + org.springframework.cloud + spring-cloud-consul-integration-tests + 3.0.2-SNAPSHOT + .. + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-consul-config + + + org.springframework.retry + spring-retry + + + org.projectlombok + lombok + + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.testcontainers + testcontainers + test + + + org.springframework.cloud + spring-cloud-consul-core + ${project.version} + test-jar + test + + + + diff --git a/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/src/main/java/org/springframework/cloud/consul/configdatatests/ConsulConfigDataRetryApplication.java b/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/src/main/java/org/springframework/cloud/consul/configdatatests/ConsulConfigDataRetryApplication.java new file mode 100644 index 00000000..c691e88c --- /dev/null +++ b/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/src/main/java/org/springframework/cloud/consul/configdatatests/ConsulConfigDataRetryApplication.java @@ -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); + } + +} diff --git a/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/src/main/resources/application.yml b/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/src/main/resources/application.yml new file mode 100644 index 00000000..69ad84bf --- /dev/null +++ b/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/src/main/resources/application.yml @@ -0,0 +1,11 @@ +server: + port: 0 + +spring: + application: + name: testConsulConfigDataIntegrationTestApp + +logging: + level: + org.springframework.cloud.consul: DEBUG + org.springframework.boot.context.config: TRACE diff --git a/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/src/test/java/org/springframework/cloud/consul/configdatatests/ConsulConfigDataRetryApplicationTests.java b/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/src/test/java/org/springframework/cloud/consul/configdatatests/ConsulConfigDataRetryApplicationTests.java new file mode 100644 index 00000000..114dbcb7 --- /dev/null +++ b/spring-cloud-consul-integration-tests/spring-cloud-consul-configdata-retry-tests/src/test/java/org/springframework/cloud/consul/configdatatests/ConsulConfigDataRetryApplicationTests.java @@ -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); + } + +}