Watch consul config and refresh when there are changes.
fixes gh-19
This commit is contained in:
@@ -22,6 +22,11 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2013-2016 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
|
||||
*
|
||||
* http://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 java.io.Closeable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import com.ecwid.consul.v1.ConsulClient;
|
||||
import com.ecwid.consul.v1.QueryParams;
|
||||
import com.ecwid.consul.v1.Response;
|
||||
import com.ecwid.consul.v1.kv.model.GetValue;
|
||||
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@CommonsLog
|
||||
public class ConfigWatch implements Closeable, ApplicationEventPublisherAware {
|
||||
private final List<String> contexts;
|
||||
private final ConsulClient consul;
|
||||
private AtomicBoolean running = new AtomicBoolean(false);
|
||||
private ApplicationEventPublisher publisher;
|
||||
private HashMap<String, Long> consulIndexes = new HashMap<>();
|
||||
|
||||
public ConfigWatch(List<String> contexts, ConsulClient consul) {
|
||||
this.contexts = contexts;
|
||||
this.consul = consul;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
this.running.compareAndSet(false, true);
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${spring.cloud.consul.config.watch.delay:10}")
|
||||
public void watchConfigKeyValues() {
|
||||
if (this.running.get()) {
|
||||
for (String context : this.contexts) {
|
||||
if (!context.endsWith("/")) {
|
||||
context = context + "/";
|
||||
}
|
||||
|
||||
try {
|
||||
Long currentIndex = this.consulIndexes.get(context);
|
||||
if (currentIndex == null) {
|
||||
currentIndex = -1L;
|
||||
}
|
||||
|
||||
Response<List<GetValue>> response = this.consul.getKVValues(context, new QueryParams(2, currentIndex));
|
||||
|
||||
Long newIndex = response.getConsulIndex();
|
||||
|
||||
if (newIndex != null && !newIndex.equals(currentIndex)) {
|
||||
// don't publish the same index again, don't publish the first time (-1) so index can be primed
|
||||
if (!this.consulIndexes.containsValue(newIndex) && !currentIndex.equals(-1L)) {
|
||||
this.publisher.publishEvent(new ConsulConfigRefreshEvent(this, new RefreshEventData(context, currentIndex, newIndex)));
|
||||
}
|
||||
this.consulIndexes.put(context, newIndex);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error initializing listener for context " + context, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.running.compareAndSet(true, false);
|
||||
}
|
||||
|
||||
@Data
|
||||
static class RefreshEventData {
|
||||
private final String context;
|
||||
private final Long prevIndex;
|
||||
private final Long newIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2013-2016 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
|
||||
*
|
||||
* http://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 com.ecwid.consul.v1.ConsulClient;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.endpoint.RefreshEndpoint;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
public class ConsulConfigAutoConfiguration {
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnClass(RefreshEndpoint.class)
|
||||
protected static class ConsulRefreshConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnBean(RefreshEndpoint.class)
|
||||
public ConsulConfigRefreshListener zookeeperConfigRefreshListener(
|
||||
RefreshEndpoint refreshEndpoint) {
|
||||
return new ConsulConfigRefreshListener(refreshEndpoint);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "spring.cloud.consul.config.watch.enabled", matchIfMissing = true)
|
||||
public ConfigWatch configWatch(ConsulPropertySourceLocator locator,
|
||||
ConsulClient consul) {
|
||||
return new ConfigWatch(locator.getContexts(), consul);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,16 @@ public class ConsulConfigProperties {
|
||||
|
||||
private String aclToken;
|
||||
|
||||
private Watch watch = new Watch();
|
||||
|
||||
@Data
|
||||
public class Watch {
|
||||
private int waitTime = 2;
|
||||
private boolean enabled = true;
|
||||
private int delay = 10;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* There are many ways in which we can specify configuration in consul i.e.,
|
||||
*
|
||||
@@ -76,7 +86,7 @@ public class ConsulConfigProperties {
|
||||
*
|
||||
* @author srikalyan.swayampakula
|
||||
*/
|
||||
public static enum Format {
|
||||
public enum Format {
|
||||
/**
|
||||
* Indicates that the configuration specified in consul is of type native key values.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2013-2016 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
|
||||
*
|
||||
* http://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.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulConfigRefreshEvent extends ApplicationEvent {
|
||||
|
||||
private Object event;
|
||||
|
||||
/**
|
||||
* Create a new ApplicationEvent.
|
||||
*
|
||||
* @param source the object on which the event initially occurred (never {@code null})
|
||||
*/
|
||||
public ConsulConfigRefreshEvent(Object source, Object event) {
|
||||
super(source);
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
public Object getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
public String getEventDesc() {
|
||||
return event.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2013-2016 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
|
||||
*
|
||||
* http://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 java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.cloud.endpoint.RefreshEndpoint;
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
import lombok.extern.apachecommons.CommonsLog;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@CommonsLog
|
||||
public class ConsulConfigRefreshListener {
|
||||
private RefreshEndpoint refresh;
|
||||
private AtomicBoolean ready = new AtomicBoolean(false);
|
||||
|
||||
public ConsulConfigRefreshListener(RefreshEndpoint refresh) {
|
||||
this.refresh = refresh;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void handle(ApplicationReadyEvent event) {
|
||||
this.ready.compareAndSet(false, true);
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void handle(ConsulConfigRefreshEvent event) {
|
||||
if (this.ready.get()) { // don't handle events before app is ready
|
||||
log.info("Event received " + event.getEventDesc());
|
||||
if (this.refresh != null) {
|
||||
String[] keys = this.refresh.refresh();
|
||||
log.info("Refresh keys changed: " + Arrays.asList(keys));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,17 @@ public class ConsulPropertySourceLocator implements PropertySourceLocator {
|
||||
|
||||
private ConsulConfigProperties properties;
|
||||
|
||||
private List<String> contexts = new ArrayList<>();
|
||||
|
||||
public ConsulPropertySourceLocator(ConsulClient consul, ConsulConfigProperties properties) {
|
||||
this.consul = consul;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public List<String> getContexts() {
|
||||
return contexts;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Retryable(interceptor = "consulRetryInterceptor")
|
||||
public PropertySource<?> locate(Environment environment) {
|
||||
@@ -55,21 +61,20 @@ public class ConsulPropertySourceLocator implements PropertySourceLocator {
|
||||
List<String> profiles = Arrays.asList(env.getActiveProfiles());
|
||||
|
||||
String prefix = this.properties.getPrefix();
|
||||
List<String> contexts = new ArrayList<>();
|
||||
|
||||
String defaultContext = prefix + "/" + this.properties.getDefaultContext();
|
||||
contexts.add(defaultContext + "/");
|
||||
addProfiles(contexts, defaultContext, profiles);
|
||||
this.contexts.add(defaultContext + "/");
|
||||
addProfiles(this.contexts, defaultContext, profiles);
|
||||
|
||||
String baseContext = prefix + "/" + appName;
|
||||
contexts.add(baseContext + "/");
|
||||
addProfiles(contexts, baseContext, profiles);
|
||||
this.contexts.add(baseContext + "/");
|
||||
addProfiles(this.contexts, baseContext, profiles);
|
||||
|
||||
CompositePropertySource composite = new CompositePropertySource("consul");
|
||||
|
||||
Collections.reverse(contexts);
|
||||
Collections.reverse(this.contexts);
|
||||
|
||||
for (String propertySourceContext : contexts) {
|
||||
for (String propertySourceContext : this.contexts) {
|
||||
ConsulPropertySource propertySource = create(propertySourceContext);
|
||||
propertySource.init();
|
||||
composite.addPropertySource(propertySource);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# Auto Configuration
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.consul.config.ConsulConfigAutoConfiguration
|
||||
|
||||
# Bootstrap Configuration
|
||||
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
|
||||
org.springframework.cloud.consul.config.ConsulConfigBootstrapConfiguration
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2013-2016 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
|
||||
*
|
||||
* http://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 java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.ecwid.consul.v1.ConsulClient;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.consul.ConsulProperties;
|
||||
import org.springframework.cloud.context.scope.refresh.RefreshScope;
|
||||
import org.springframework.cloud.endpoint.RefreshEndpoint;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class ConsulPropertySourceLocatorTests {
|
||||
public static final String PREFIX = "_propertySourceLocatorTests_config__";
|
||||
public static final String ROOT = PREFIX + UUID.randomUUID();
|
||||
private ConfigurableApplicationContext context;
|
||||
public static final String KEY = ROOT + "/application/testProp";
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
static class Config {
|
||||
@Bean
|
||||
public RefreshEndpoint refreshEndpoint(ConfigurableApplicationContext context,
|
||||
RefreshScope scope) {
|
||||
RefreshEndpoint endpoint = new TestRefreshEndpoint(context, scope);
|
||||
return endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
static class TestRefreshEndpoint extends RefreshEndpoint {
|
||||
private CountDownLatch successLatch = new CountDownLatch(1);
|
||||
private CountDownLatch toManyLatch = new CountDownLatch(1);
|
||||
private AtomicInteger count = new AtomicInteger();
|
||||
|
||||
public TestRefreshEndpoint( ConfigurableApplicationContext context, RefreshScope scope) {
|
||||
super(context, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String[] refresh() {
|
||||
String[] keys = super.refresh();
|
||||
if (this.count.incrementAndGet() == 1) {
|
||||
this.successLatch.countDown();
|
||||
} else {
|
||||
this.toManyLatch.countDown();
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigurableEnvironment environment;
|
||||
private ConsulClient client;
|
||||
private ConsulProperties properties;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.properties = new ConsulProperties();
|
||||
this.client = new ConsulClient(properties.getHost(), properties.getPort());
|
||||
this.client.deleteKVValues(PREFIX);
|
||||
this.client.setKVValue(KEY, "testPropVal");
|
||||
|
||||
|
||||
this.context = new SpringApplicationBuilder(Config.class)
|
||||
.web(false)
|
||||
.run("--spring.spring.application.name=testConsulPropertySourceLocator",
|
||||
"--spring.cloud.consul.config.prefix="+ROOT,
|
||||
"spring.cloud.consul.config.watch.delay=1");
|
||||
|
||||
this.client = context.getBean(ConsulClient.class);
|
||||
this.properties = context.getBean(ConsulProperties.class);
|
||||
this.environment = context.getEnvironment();
|
||||
}
|
||||
|
||||
@After
|
||||
public void teardown() {
|
||||
this.client.deleteKVValues(PREFIX);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void propertyLoadedAndUpdated() throws Exception {
|
||||
String testProp = this.environment.getProperty("testProp");
|
||||
assertThat("testProp was wrong", testProp, is(equalTo("testPropVal")));
|
||||
|
||||
this.client.setKVValue(KEY, "testPropValUpdate");
|
||||
|
||||
TestRefreshEndpoint endpoint = this.context.getBean(TestRefreshEndpoint.class);
|
||||
boolean receivedEvent = endpoint.successLatch.await(5, TimeUnit.SECONDS);
|
||||
assertThat("listener didn't receive event", receivedEvent, is(true));
|
||||
|
||||
testProp = this.environment.getProperty("testProp");
|
||||
assertThat("testProp was wrong after update", testProp, is(equalTo("testPropValUpdate")));
|
||||
|
||||
boolean receivedExtraEvent = endpoint.toManyLatch.await(500, TimeUnit.MILLISECONDS);
|
||||
assertThat("refresh called to many times", receivedExtraEvent, is(false));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user