Java tutorial
/** * Copyright (C) 2015 meltmedia (christian.trimble@meltmedia.com) * * 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 com.meltmedia.dropwizard.etcd.json; import static java.lang.String.format; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.Gauge; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import mousio.etcd4j.EtcdClient; import mousio.etcd4j.responses.EtcdException; import mousio.etcd4j.responses.EtcdKeysResponse; import mousio.etcd4j.responses.EtcdKeysResponse.EtcdNode; /** * A service for watching changes on Etcd with JSON content stored in the values. * * @author Christian Trimble * */ public class WatchService { public static final Logger logger = LoggerFactory.getLogger(WatchService.class); public static class Builder { private Supplier<EtcdClient> client; private String directory; private ScheduledExecutorService executor; private ObjectMapper mapper; private MetricRegistry registry; private long timeout = 30L; private TimeUnit timeoutUnit = TimeUnit.SECONDS; public Builder withEtcdClient(Supplier<EtcdClient> client) { this.client = client; return this; } public Builder withDirectory(String directory) { this.directory = directory; return this; } public Builder withExecutor(ScheduledExecutorService executor) { this.executor = executor; return this; } public Builder withMapper(ObjectMapper mapper) { this.mapper = mapper; return this; } public Builder withMetricRegistry(MetricRegistry registry) { this.registry = registry; return this; } public Builder withWatchTimeout(long timeout, TimeUnit timeoutUnit) { this.timeout = timeout; this.timeoutUnit = timeoutUnit; return this; } public WatchService build() { if (registry == null) { throw new IllegalStateException("metric registry is required"); } if (timeout <= 0L) { throw new IllegalStateException("timeout must be positive"); } if (timeoutUnit == null) { throw new IllegalStateException("timeout unit is required"); } return new WatchService(client, directory, mapper, executor, registry, timeout, timeoutUnit); } } public static String ETCD_INDEX = "etcdIndex"; public static String SYNC_INDEX = "syncIndex"; public static String INDEX_DELTA = "indexDelta"; public static String WATCH_EVENTS = "watchEvents"; public static String RESYNC_EVENTS = "resyncEvents"; private Supplier<EtcdClient> client; private ObjectMapper mapper; private String directory; private ScheduledExecutorService executor; private long timeout; private TimeUnit timeoutUnit; private ScheduledFuture<?> watchFuture; private AtomicLong syncIndex = new AtomicLong(0); // the index to which we are synchronized. private AtomicLong etcdIndex = new AtomicLong(0); // the last etcd index we have seen. private AtomicLong indexDelta = new AtomicLong(0); private List<Watch> watchers = Lists.newCopyOnWriteArrayList(); private FunctionalLock lock = new FunctionalLock(); private Meter watchEvents; private Meter resyncEvents; protected WatchService(Supplier<EtcdClient> client, String directory, ObjectMapper mapper, ScheduledExecutorService executor, MetricRegistry registry, long timeout, TimeUnit timeoutUnit) { this.client = client; this.directory = directory; this.mapper = mapper; this.executor = executor; this.timeout = timeout; this.timeoutUnit = timeoutUnit; registry.register(MetricRegistry.name(WatchService.class, ETCD_INDEX), (Gauge<Long>) () -> etcdIndex.get()); registry.register(MetricRegistry.name(WatchService.class, SYNC_INDEX), (Gauge<Long>) () -> syncIndex.get()); registry.register(MetricRegistry.name(WatchService.class, INDEX_DELTA), (Gauge<Long>) () -> indexDelta.get()); watchEvents = registry.meter(MetricRegistry.name(WatchService.class, WATCH_EVENTS)); resyncEvents = registry.meter(MetricRegistry.name(WatchService.class, RESYNC_EVENTS)); } public <T> Watch registerDirectoryWatch(String directory, TypeReference<T> type, EtcdEventHandler<T> handler) { DirectoryWatch<T> watch = new DirectoryWatch<T>(this.directory + directory, type, handler); addWatch(watch); return watch; } public <T> Watch registerValueWatch(String directory, String key, TypeReference<T> type, EtcdEventHandler<T> handler) { ValueWatch<T> watch = new ValueWatch<T>(this.directory + directory, key, type, handler); addWatch(watch); return watch; } public void start() { logger.debug("starting watch for {}", directory); ensureDirectoryExists(); startWatchingNodes(); logger.debug("started watch for {}", directory); } public void stop() { logger.debug("stopping watch for {}", directory); stopWatchingNodes(); logger.debug("stopped watch for {}", directory); } public List<Watch> outOfSyncWatchers() { return lock.readSupplier(() -> watchers.stream().filter(w -> !w.inSync()).collect(Collectors.toList())) .get(); } protected long getWatchIndex() { lock.read.lock(); try { return syncIndex.get(); } finally { lock.read.unlock(); } } protected void startWatchingNodes() { logger.debug("starting watch thread."); watchFuture = executor.scheduleWithFixedDelay(() -> { logger.debug(format("watch returned for %s", syncIndex.get())); long currIndex = lock.readSupplier(syncIndex::get).get(); try { // get the next event. EtcdKeysResponse response = client.get().getDir(directory).recursive().waitForChange(currIndex + 1) .timeout(timeout, timeoutUnit).send().get(); watchEvents.mark(); lock.writeRunnable(() -> { etcdIndex.set(response.etcdIndex); if (syncIndex.compareAndSet(currIndex, response.node.modifiedIndex)) { indexDelta.set(etcdIndex.get() - syncIndex.get()); watchers.forEach(w -> w.accept(response)); } }).run(); } catch (EtcdException etcdException) { // we don't know what the state is, resync. resyncEvents.mark(); ensureDirectoryExists(); lock.writeRunnable(() -> { etcdIndex.set(etcdException.index); syncIndex.set(watchers.stream().mapToLong(w -> w.sync().currentIndex()).min() .orElse(etcdException.index)); indexDelta.set(etcdIndex.get() - syncIndex.get()); }).run(); } catch (TimeoutException e) { logger.debug("etcd watch timed out"); } catch (Exception e) { logger.warn(format("exception thrown while watching %s", directory), e); } }, 1, 1, TimeUnit.MILLISECONDS); } protected void addWatch(Watch watch) { lock.writeRunnable(() -> { watch.sync(); watchers.add(watch); syncIndex.set(watchers.stream().mapToLong(Watch::currentIndex).min().orElse(0L)); indexDelta.set(etcdIndex.get() - syncIndex.get()); }).run(); } protected void removeWatch(Watch watch) { lock.writeRunnable(() -> { watchers.remove(watch); }).run(); } protected void stopWatchingNodes() { if (watchFuture != null) { watchFuture.cancel(true); } } static <T> Optional<EtcdEvent<T>> asEvent(ObjectMapper mapper, String directory, EtcdKeysResponse response, TypeReference<T> type) { T value = readValue(mapper, response.node, type); T prevValue = readValue(mapper, response.prevNode, type); long etcdIndex = response.node.modifiedIndex; if (value == null && prevValue == null) return Optional.empty(); switch (response.action) { case delete: return Optional.of(EtcdEvent.<T>builder().withType(EtcdEvent.Type.removed).withIndex(etcdIndex) .withValue(value).withPrevValue(prevValue) .withKey(removeDirectory(directory, response.prevNode.key)).build()); case expire: return Optional.of(EtcdEvent.<T>builder().withType(EtcdEvent.Type.removed).withIndex(etcdIndex) .withValue(value).withPrevValue(prevValue) .withKey(removeDirectory(directory, response.prevNode.key)).build()); case set: if (filterValueChange(value, prevValue)) { return Optional.of(EtcdEvent.<T>builder() .withType(prevValue == null ? EtcdEvent.Type.added : EtcdEvent.Type.updated) .withIndex(etcdIndex).withValue(value).withPrevValue(prevValue) .withKey(removeDirectory(directory, response.node.key)).build()); } break; case update: if (!value.equals(prevValue)) { return Optional.of(EtcdEvent.<T>builder().withType(EtcdEvent.Type.updated).withIndex(etcdIndex) .withValue(value).withPrevValue(prevValue) .withKey(removeDirectory(directory, response.node.key)).build()); } break; case compareAndSwap: if (!value.equals(prevValue)) { return Optional.of(EtcdEvent.<T>builder().withType(EtcdEvent.Type.updated).withIndex(etcdIndex) .withValue(value).withPrevValue(prevValue) .withKey(removeDirectory(directory, response.node.key)).build()); } default: break; } return Optional.empty(); } public interface Watch { public Watch accept(EtcdKeysResponse response); public Watch sync(); public Long currentIndex(); public void stop(); public boolean inSync(); } protected static class EtcdValue<T> { T value; long modifiedIndex; long loadIndex; public EtcdValue(T value, long modifiedIndex, long loadIndex) { this.value = value; this.modifiedIndex = modifiedIndex; this.loadIndex = loadIndex; } } public class DirectoryWatch<T> implements Watch { TypeReference<T> type; EtcdEventHandler<T> handler; String directory; volatile Long currentIndex = Long.valueOf(0L); volatile boolean inSync = false; Pattern keyMatcher; Map<String, EtcdValue<T>> state = Maps.newHashMap(); private DirectoryWatch(String directory, TypeReference<T> type, EtcdEventHandler<T> handler) { this.directory = directory; this.type = type; this.handler = handler; this.keyMatcher = Pattern.compile("\\A" + Pattern.quote(directory) + "/[^/]+\\Z"); } public String toString() { return String.format("Directory watch of %s", directory); } public Long currentIndex() { return currentIndex; } public boolean inSync() { return inSync; } public Watch sync() { inSync = false; try { EtcdKeysResponse dirList = client.get().getDir(directory).dir().send().get(); currentIndex = dirList.etcdIndex; if (dirList.node.nodes != null) { dirList.node.nodes.stream().filter(n -> !n.dir).forEach(node -> { state.compute(removeDirectory(directory, node.key), (key, entry) -> { T value = readValue(mapper, node, type); if (entry == null && value == null) return null; else if (entry == null) { fireEvent(handler, EtcdEvent.<T>builder().withType(EtcdEvent.Type.added) .withIndex(node.modifiedIndex).withValue(value).withKey(key).build()); return new EtcdValue<T>(value, node.modifiedIndex, currentIndex); } else if (value == null) { fireEvent(handler, EtcdEvent.<T>builder().withType(EtcdEvent.Type.removed) .withIndex(node.modifiedIndex).withPrevValue(entry.value) .withKey(key).build()); return null; } else if (entry.modifiedIndex == node.modifiedIndex || entry.equals(value)) { entry.loadIndex = currentIndex; return entry; } else { fireEvent(handler, EtcdEvent.<T>builder().withType(EtcdEvent.Type.updated) .withIndex(node.modifiedIndex).withValue(value) .withPrevValue(entry.value).withKey(key).build()); return new EtcdValue<T>(value, node.modifiedIndex, currentIndex); } }); }); } state.replaceAll((key, entry) -> { if (entry.loadIndex == currentIndex) return entry; fireEvent(handler, EtcdEvent.<T>builder().withType(EtcdEvent.Type.removed) .withIndex(currentIndex).withPrevValue(entry.value).withKey(key).build()); return null; }); inSync = true; } catch (EtcdException ee) { currentIndex = Long.valueOf((long) ee.index.intValue()); if (ee.errorCode == 100) { state.replaceAll((key, entry) -> { fireEvent(handler, EtcdEvent.<T>builder().withType(EtcdEvent.Type.removed) .withIndex(currentIndex).withPrevValue(entry.value).withKey(key).build()); return null; }); inSync = true; } else { logger.debug("exception during sync", ee); } } catch (IOException | TimeoutException e) { logger.error(format("failed to start watch for directory %s", directory), e); } return this; } public Watch accept(EtcdKeysResponse response) { if (!keyMatcher.matcher(key(response)).matches()) { return this; } Long responseIndex = indexOf(response); if (currentIndex < responseIndex) { asEvent(mapper, directory, response, type).ifPresent(event -> { state.compute(event.getKey(), (key, entry) -> { switch (event.getType()) { case added: fireEvent(handler, event); return new EtcdValue<T>(event.getValue(), response.node.modifiedIndex, response.etcdIndex); case updated: fireEvent(handler, event); return new EtcdValue<T>(event.getValue(), response.node.modifiedIndex, response.etcdIndex); case removed: fireEvent(handler, event); return null; default: throw new IllegalStateException("unknown event type " + event.getType().name()); } }); }); currentIndex = responseIndex; } else { logger.debug("filtering event for {}", key(response)); logger.debug("response index {} current index {}", responseIndex, currentIndex); } return this; } public void stop() { removeWatch(this); } } public class ValueWatch<T> implements Watch { TypeReference<T> type; EtcdEventHandler<T> handler; String directory; String key; volatile boolean inSync = false; volatile Long currentIndex = Long.valueOf(0L); volatile EtcdValue<T> currentValue = null; protected ValueWatch(String directory, String key, TypeReference<T> type, EtcdEventHandler<T> handler) { this.directory = directory; this.key = key; this.type = type; this.handler = handler; } @Override public Watch accept(EtcdKeysResponse response) { Long responseIndex = indexOf(response); if (currentIndex < responseIndex && key(response).equals(directory + "/" + key)) { asEvent(mapper, directory, response, type).ifPresent(handler::handle); currentIndex = responseIndex; } return this; } public boolean inSync() { return inSync; } @Override public Watch sync() { inSync = false; try { EtcdKeysResponse response = client.get().get(directory + "/" + key).send().get(); currentIndex = response.etcdIndex; if (!response.node.dir) { T value = readValue(mapper, response.node, type); if (value != null && currentValue == null) { fireEvent(handler, EtcdEvent.<T>builder().withType(EtcdEvent.Type.added).withValue(value) .withKey(key).withIndex(response.node.modifiedIndex).build()); currentValue = new EtcdValue<T>(value, response.node.modifiedIndex, currentIndex); } else if (value == null && currentValue != null) { fireEvent(handler, EtcdEvent.<T>builder().withType(EtcdEvent.Type.removed) .withPrevValue(currentValue.value).withIndex(response.node.modifiedIndex) .withKey(key).build()); currentValue = null; } else if (value == null && currentValue == null) { } else if (response.node.modifiedIndex == currentValue.modifiedIndex || currentValue.value.equals(value)) { currentValue = new EtcdValue<T>(currentValue.value, currentValue.modifiedIndex, currentIndex); } inSync = true; } } catch (EtcdException ee) { currentIndex = ee.index; } catch (IOException | TimeoutException e) { logger.error(format("faled to start watch for directory %s", directory), e); } return this; } @Override public Long currentIndex() { return currentIndex; } public void stop() { removeWatch(this); } } protected void ensureDirectoryExists() { try { EtcdKeysResponse response = client.get().putDir(directory).isDir().send().get(); lock.writeRunnable(() -> { syncIndex.set(response.node.modifiedIndex); etcdIndex.set(response.node.modifiedIndex); }); } catch (EtcdException ee) { lock.writeRunnable(() -> { syncIndex.set(ee.index); etcdIndex.set(ee.index); }); } catch (Exception e) { throw new EtcdDirectoryException(String.format("could not create directory %s", directory), e); } } static String key(EtcdKeysResponse response) { String key = response.node != null ? response.node.key : response.prevNode != null ? response.prevNode.key : null; if (key == null) throw new IllegalStateException("no key defined."); return key; } static <T> T readValue(ObjectMapper mapper, EtcdNode value, TypeReference<T> type) { try { return value == null || value.value == null || value.dir ? null : mapper.readValue(value.value, type); } catch (Exception e) { logger.warn(format("could not read value at %s as type %s", value.key, type), e); return null; } } static <T> void fireEvent(EtcdEventHandler<T> handler, EtcdEvent<T> event) { try { handler.handle(event); } catch (Throwable t) { logger.error(format("exception thrown handling event for %s", event.getKey()), t); } } static String removeDirectory(String directory, String key) { return key.substring(directory.length() + 1, key.length()); } static <T> boolean filterValueChange(T value, T prevValue) { return value == null || prevValue == null || !value.equals(prevValue); } static Long indexOf(EtcdKeysResponse response) { return response.node != null ? response.node.modifiedIndex : response.prevNode != null ? response.prevNode.modifiedIndex : response.etcdIndex; } public static Builder builder() { return new Builder(); } }