/* * Copyright 2013-2019 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 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; 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.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import static org.springframework.cloud.consul.config.ConsulConfigProperties.Format.FILES; /** * @author Spencer Gibb */ public class ConfigWatch implements ApplicationEventPublisherAware, SmartLifecycle { private static final Log log = LogFactory.getLog(ConfigWatch.class); private final ConsulConfigProperties properties; private final ConsulClient consul; private final TaskScheduler taskScheduler; private final AtomicBoolean running = new AtomicBoolean(false); private LinkedHashMap<String, Long> consulIndexes; private ApplicationEventPublisher publisher; private boolean firstTime = true; private ScheduledFuture<?> watchFuture; public ConfigWatch(ConsulConfigProperties properties, ConsulClient consul, LinkedHashMap<String, Long> initialIndexes) { this(properties, consul, initialIndexes, getTaskScheduler()); } public ConfigWatch(ConsulConfigProperties properties, ConsulClient consul, LinkedHashMap<String, Long> initialIndexes, TaskScheduler taskScheduler) { this.properties = properties; this.consul = consul; this.consulIndexes = new LinkedHashMap<>(initialIndexes); this.taskScheduler = taskScheduler; } private static ThreadPoolTaskScheduler getTaskScheduler() { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.initialize(); return taskScheduler; } @Override public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } @Override public void start() { 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(); } @Timed("consul.watch-config-keys") public void watchConfigKeyValues() { if (this.running.get()) { for (String context : this.consulIndexes.keySet()) { // turn the context into a Consul folder path (unless our config format // are FILES) if (this.properties.getFormat() != FILES && !context.endsWith("/")) { context = context + "/"; } try { Long currentIndex = this.consulIndexes.get(context); if (currentIndex == null) { currentIndex = -1L; } log.trace("watching consul for context '" + context + "' with index " + currentIndex); // use the consul ACL token if found String aclToken = this.properties.getAclToken(); if (StringUtils.isEmpty(aclToken)) { aclToken = null; } Response<List<GetValue>> response = this.consul.getKVValues(context, aclToken, new QueryParams(this.properties.getWatch().getWaitTime(), currentIndex)); // if response.value == null, response was a 404, otherwise it was a // 200 // reducing churn if there wasn't anything if (response.getValue() != null && !response.getValue().isEmpty()) { 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)) { log.trace("Context " + context + " has new index " + newIndex); RefreshEventData data = new RefreshEventData(context, currentIndex, newIndex); this.publisher.publishEvent( new RefreshEvent(this, data, data.toString())); } else if (log.isTraceEnabled()) { log.trace("Event for index already published for context " + context); } this.consulIndexes.put(context, newIndex); } else if (log.isTraceEnabled()) { log.trace("Same index for context " + context); } } else if (log.isTraceEnabled()) { log.trace("No value for context " + context); } } catch (Exception e) { // only fail fast on the initial query, otherwise just log the error if (this.firstTime && this.properties.isFailFast()) { log.error( "Fail fast is set and there was an error reading configuration from consul."); ReflectionUtils.rethrowRuntimeException(e); } else if (log.isTraceEnabled()) { log.trace("Error querying consul Key/Values for context '" + context + "'", e); } else if (log.isWarnEnabled()) { // simplified one line log message in the event of an agent // failure log.warn("Error querying consul Key/Values for context '" + context + "'. Message: " + e.getMessage()); } } } } this.firstTime = false; } public static class RefreshEventData { private final String context; private final Long prevIndex; private final Long newIndex; RefreshEventData(String context, Long prevIndex, Long newIndex) { this.context = context; this.prevIndex = prevIndex; this.newIndex = newIndex; } public String getContext() { return this.context; } public Long getPrevIndex() { return this.prevIndex; } public Long getNewIndex() { return this.newIndex; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } RefreshEventData that = (RefreshEventData) o; return Objects.equals(this.context, that.context) && Objects.equals(this.prevIndex, that.prevIndex) && Objects.equals(this.newIndex, that.newIndex); } @Override public int hashCode() { return Objects.hash(this.context, this.prevIndex, this.newIndex); } @Override public String toString() { return new ToStringCreator(this).append("context", this.context) .append("prevIndex", this.prevIndex).append("newIndex", this.newIndex) .toString(); } } }