package com.meltmedia.dropwizard.etcd.cluster; import java.util.Collections; import java.util.Comparator; import java.util.Objects; import java.util.SortedSet; import java.util.function.BiFunction; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.meltmedia.dropwizard.etcd.json.EtcdEvent; import com.meltmedia.dropwizard.etcd.json.EtcdJson.MappedEtcdDirectory; import com.meltmedia.dropwizard.etcd.json.WatchService.Watch; public class ProcessorStateTracker { private static Logger logger = LoggerFactory.getLogger(ProcessorStateTracker.class); public static class Builder { private MappedEtcdDirectory<ProcessorNode> directory; private ClusterNode thisNode; public Builder withDirectory(MappedEtcdDirectory<ProcessorNode> directory) { this.directory = directory; return this; } public Builder withThisNode(ClusterNode thisNode) { this.thisNode = thisNode; return this; } public ProcessorStateTracker build() { return new ProcessorStateTracker(directory, thisNode); } } public static Builder builder() { return new Builder(); } private MappedEtcdDirectory<ProcessorNode> directory; private Watch watch; private volatile ProcessorState state; private ClusterNode thisNode; private ProcessorStateTracker(MappedEtcdDirectory<ProcessorNode> directory, ClusterNode thisNode) { this.directory = directory; this.state = ProcessorState.empty(); this.thisNode = thisNode; } public Runnable start( Runnable startWithTracker ) { return ()->{ watch = directory.registerWatch(this::handle); try { startWithTracker.run(); } finally { publishNode(); } }; } public Runnable stop(Runnable stopWithTracker) { return ()->{ unpublishNode(); try { stopWithTracker.run(); } finally { watch.stop(); } }; } public ProcessorState getState() { return state; } public void handle(EtcdEvent<ProcessorNode> event) { switch (event.getType()) { case added: state = state.addProcessor(event.getIndex(), event.getValue()); break; case removed: state = state.removeProcessor(event.getIndex(), event.getPrevValue()); break; case updated: state = state.removeProcessor(event.getIndex(), event.getPrevValue()).addProcessor(event.getIndex(), event.getValue()); break; } } public static class ProcessorState { private static Comparator<ProcessorNode> COMPARATOR = Ordering.<ProcessorNode> from( (n1, n2) -> Objects.compare(n1.getStartedAt(), n2.getStartedAt(), Ordering.natural())) .compound((n1, n2) -> Objects.compare(n1.getId(), n2.getId(), Ordering.natural())); public static ProcessorState empty() { return new ProcessorState(0, Collections.emptySortedSet()); } public static ProcessorState empty( long lastModifiedIndex ) { return new ProcessorState(lastModifiedIndex, Collections.emptySortedSet()); } private SortedSet<ProcessorNode> processors; private final long lastModifiedIndex; private ProcessorState(long lastModifiedIndex, SortedSet<ProcessorNode> processors) { this.lastModifiedIndex = lastModifiedIndex; this.processors = processors; } public ProcessorState addProcessor(long lastModifiedIndex, ProcessorNode newProcessor) { return new ProcessorState(lastModifiedIndex, newProcessors(newProcessor, SortedSet::add)); } public ProcessorState removeProcessor(long lastModfiedIndex, ProcessorNode oldProcessor) { return new ProcessorState(lastModifiedIndex, newProcessors(oldProcessor, SortedSet::remove)); } private SortedSet<ProcessorNode> newProcessors(ProcessorNode changingMember, BiFunction<SortedSet<ProcessorNode>, ProcessorNode, Boolean> action) { SortedSet<ProcessorNode> newProcessors = Sets.newTreeSet(COMPARATOR); newProcessors.addAll(processors); action.apply(newProcessors, changingMember); return newProcessors; } public int processorCount() { return processors.size(); } public long lastModifiedIndex() { return lastModifiedIndex; } public SortedSet<ProcessorNode> getProcessors() { return Collections.unmodifiableSortedSet(processors); } public boolean hasProcessor(String nodeId) { return processors.stream().anyMatch(m -> Objects.equals(m.getId(), nodeId) ); } } void publishNode() { directory.newDao().put(thisNode.getId(), new ProcessorNode().withId(thisNode.getId()).withStartedAt(new DateTime())); } void unpublishNode() { try { directory.newDao().remove(thisNode.getId()); } catch( RuntimeException re ) { logger.warn("could not unpublish process node"); } } void removeCrashedProcessor( ProcessorNode p ) { try { directory.newDao().remove(p.getId()); } catch( Exception e ) { logger.debug("could not remove crashed processor node"); } } }