From 79d703a3db64c16e0533d99daf502036e19f6486 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Tue, 5 Jun 2018 14:19:03 -0400 Subject: [PATCH] Moves ConfigWatch from @Scheduled to TaskScheduler fixes gh-399 --- .../main/asciidoc/spring-cloud-consul.adoc | 4 +- .../cloud/consul/config/ConfigWatch.java | 75 ++++++++++++++----- .../config/ConsulConfigAutoConfiguration.java | 16 +++- .../cloud/consul/config/ConfigWatchTests.java | 3 +- 4 files changed, 77 insertions(+), 21 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-consul.adoc b/docs/src/main/asciidoc/spring-cloud-consul.adoc index 84236b6d..7ed06d00 100644 --- a/docs/src/main/asciidoc/spring-cloud-consul.adoc +++ b/docs/src/main/asciidoc/spring-cloud-consul.adoc @@ -218,10 +218,12 @@ spring: The Consul Config Watch takes advantage of the ability of consul to https://www.consul.io/docs/agent/watches.html#keyprefix[watch a key prefix]. The Config Watch makes a blocking Consul HTTP API call to determine if any relevant configuration data has changed for the current application. If there is new configuration data a Refresh Event is published. This is equivalent to calling the `/refresh` actuator endpoint. -To change the frequency of when the Config Watch is called change `spring.cloud.consul.config.watch.delay`. The default value is 1000, which is in milliseconds. +To change the frequency of when the Config Watch is called change `spring.cloud.consul.config.watch.delay`. The default value is 1000, which is in milliseconds. The delay is the amount of time after the end of the previous invocation and the start of the next. To disable the Config Watch set `spring.cloud.consul.config.watch.enabled=false`. +The watch uses a Spring `TaskScheduler` to schedule the call to consul. By default it is a `ThreadPoolTaskScheduler` with a `poolSize` of 1. To change the `TaskScheduler`, create a bean of type `TaskScheduler` named with the `ConsulConfigAutoConfiguration.CONFIG_WATCH_TASK_SCHEDULER_NAME` constant. + [[spring-cloud-consul-config-format]] === YAML or Properties with Config diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java index c5ef893a..74b854f9 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConfigWatch.java @@ -16,6 +16,12 @@ package org.springframework.cloud.consul.config; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicBoolean; + import com.ecwid.consul.v1.ConsulClient; import com.ecwid.consul.v1.QueryParams; import com.ecwid.consul.v1.Response; @@ -23,41 +29,51 @@ import com.ecwid.consul.v1.kv.model.GetValue; import io.micrometer.core.annotation.Timed; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.cloud.endpoint.event.RefreshEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.SmartLifecycle; import org.springframework.core.style.ToStringCreator; -import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import javax.annotation.PostConstruct; -import java.io.Closeable; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; - import static org.springframework.cloud.consul.config.ConsulConfigProperties.Format.FILES; /** * @author Spencer Gibb */ -public class ConfigWatch implements Closeable, ApplicationEventPublisherAware { +public class ConfigWatch implements ApplicationEventPublisherAware, SmartLifecycle { private static final Log log = LogFactory.getLog(ConfigWatch.class); private final ConsulConfigProperties properties; private final ConsulClient consul; private LinkedHashMap consulIndexes; + private final TaskScheduler taskScheduler; private final AtomicBoolean running = new AtomicBoolean(false); private ApplicationEventPublisher publisher; private boolean firstTime = true; + private ScheduledFuture watchFuture; public ConfigWatch(ConsulConfigProperties properties, ConsulClient consul, LinkedHashMap initialIndexes) { + this(properties, consul, initialIndexes, getTaskScheduler()); + } + + private static ThreadPoolTaskScheduler getTaskScheduler() { + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.initialize(); + return taskScheduler; + } + + public ConfigWatch(ConsulConfigProperties properties, ConsulClient consul, LinkedHashMap initialIndexes, + TaskScheduler taskScheduler) { this.properties = properties; this.consul = consul; this.consulIndexes = new LinkedHashMap<>(initialIndexes); + this.taskScheduler = taskScheduler; } @Override @@ -65,12 +81,42 @@ public class ConfigWatch implements Closeable, ApplicationEventPublisherAware { this.publisher = publisher; } - @PostConstruct + @Override public void start() { - this.running.compareAndSet(false, true); + if (this.running.compareAndSet(false, true)) { + this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(this::watchConfigKeyValues, + this.properties.getWatch().getDelay()); + } + } + + @Override + public boolean isAutoStartup() { + return true; + } + + @Override + public void stop(Runnable callback) { + this.stop(); + callback.run(); + } + + @Override + public int getPhase() { + return 0; + } + + @Override + public void stop() { + if (this.running.compareAndSet(true, false) && this.watchFuture != null) { + this.watchFuture.cancel(true); + } + } + + @Override + public boolean isRunning() { + return this.running.get(); } - @Scheduled(fixedDelayString = "${spring.cloud.consul.config.watch.delay:1000}") @Timed(value ="consul.watch-config-keys") public void watchConfigKeyValues() { if (this.running.get()) { @@ -138,11 +184,6 @@ public class ConfigWatch implements Closeable, ApplicationEventPublisherAware { firstTime = false; } - @Override - public void close() { - this.running.compareAndSet(true, false); - } - static class RefreshEventData { private final String context; private final Long prevIndex; diff --git a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java index 61770dbf..9bdc113b 100644 --- a/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java +++ b/spring-cloud-consul-config/src/main/java/org/springframework/cloud/consul/config/ConsulConfigAutoConfiguration.java @@ -18,12 +18,15 @@ package org.springframework.cloud.consul.config; import com.ecwid.consul.v1.ConsulClient; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.consul.ConditionalOnConsulEnabled; import org.springframework.cloud.endpoint.RefreshEndpoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; /** * @author Spencer Gibb @@ -33,14 +36,23 @@ import org.springframework.context.annotation.Configuration; @ConditionalOnProperty(name = "spring.cloud.consul.config.enabled", matchIfMissing = true) public class ConsulConfigAutoConfiguration { + public static final String CONFIG_WATCH_TASK_SCHEDULER_NAME = "configWatchTaskScheduler"; + @Configuration @ConditionalOnClass(RefreshEndpoint.class) protected static class ConsulRefreshConfiguration { @Bean @ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled", matchIfMissing = true) public ConfigWatch configWatch(ConsulConfigProperties properties, - ConsulPropertySourceLocator locator, ConsulClient consul) { - return new ConfigWatch(properties, consul, locator.getContextIndexes()); + ConsulPropertySourceLocator locator, ConsulClient consul, + @Qualifier(CONFIG_WATCH_TASK_SCHEDULER_NAME) TaskScheduler taskScheduler) { + return new ConfigWatch(properties, consul, locator.getContextIndexes(), taskScheduler); + } + + @Bean(name = CONFIG_WATCH_TASK_SCHEDULER_NAME) + @ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled", matchIfMissing = true) + public TaskScheduler configWatchTaskScheduler() { + return new ThreadPoolTaskScheduler(); } } } diff --git a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConfigWatchTests.java b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConfigWatchTests.java index fa3533dd..37ed4511 100644 --- a/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConfigWatchTests.java +++ b/spring-cloud-consul-config/src/test/java/org/springframework/cloud/consul/config/ConfigWatchTests.java @@ -35,6 +35,7 @@ import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -60,7 +61,7 @@ public class ConfigWatchTests { setupWatch(eventPublisher, new GetValue(), "/app/", "2ee647bd-bd69-4118-9f34-b9a6e9e60746"); - verify(eventPublisher, times(1)).publishEvent(any(RefreshEvent.class)); + verify(eventPublisher, atLeastOnce()).publishEvent(any(RefreshEvent.class)); } @Test