/** * 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.cluster; import java.util.Collections; import java.util.Comparator; import java.util.Objects; import java.util.SortedSet; import java.util.function.BiFunction; 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 ClusterStateTracker { public static class Builder { private MappedEtcdDirectory<ClusterNode> directory; private ClusterNode thisNode; public Builder withDirectory(MappedEtcdDirectory<ClusterNode> directory) { this.directory = directory; return this; } public Builder withThisNode(ClusterNode thisNode) { this.thisNode = thisNode; return this; } public ClusterStateTracker build() { return new ClusterStateTracker(directory, thisNode); } } public static Builder builder() { return new Builder(); } private MappedEtcdDirectory<ClusterNode> directory; private Watch watch; private volatile State state; private ClusterStateTracker(MappedEtcdDirectory<ClusterNode> directory, ClusterNode thisNode) { this.directory = directory; this.state = State.empty(); } public void start() { watch = directory.registerWatch(this::handle); } public void stop() { watch.stop(); } public State getState() { return state; } public void handle(EtcdEvent<ClusterNode> event) { switch (event.getType()) { case added: state = state.addMember(event.getIndex(), event.getValue()); break; case removed: state = state.removeMember(event.getIndex(), event.getPrevValue()); break; case updated: state = state.removeMember(event.getIndex(), event.getPrevValue()).addMember(event.getIndex(), event.getValue()); break; } } public static class State { private static Comparator<ClusterNode> COMPARATOR = Ordering.<ClusterNode> from( (n1, n2) -> Objects.compare(n1.getStartedAt(), n2.getStartedAt(), Ordering.natural())) .compound((n1, n2) -> Objects.compare(n1.getId(), n2.getId(), Ordering.natural())); public static State empty() { return new State(0, Collections.emptySortedSet()); } public static State empty( long lastModifiedIndex ) { return new State(lastModifiedIndex, Collections.emptySortedSet()); } private SortedSet<ClusterNode> members; private final long lastModifiedIndex; private State(long lastModifiedIndex, SortedSet<ClusterNode> members) { this.lastModifiedIndex = lastModifiedIndex; this.members = members; } public ClusterNode master() { return members.isEmpty() ? null : members.first(); } public State addMember(long lastModifiedIndex, ClusterNode newMember) { return new State(lastModifiedIndex, newMembers(newMember, SortedSet::add)); } public State removeMember(long lastModfiedIndex, ClusterNode oldMember) { return new State(lastModifiedIndex, newMembers(oldMember, SortedSet::remove)); } private SortedSet<ClusterNode> newMembers(ClusterNode changingMember, BiFunction<SortedSet<ClusterNode>, ClusterNode, Boolean> action) { SortedSet<ClusterNode> newMembers = Sets.newTreeSet(COMPARATOR); newMembers.addAll(members); action.apply(newMembers, changingMember); return newMembers; } public int memberCount() { return members.size(); } public long lastModifiedIndex() { return lastModifiedIndex; } public SortedSet<ClusterNode> getMembers() { return Collections.unmodifiableSortedSet(members); } public boolean hasMember(String nodeId) { return members.stream().anyMatch(m -> m.getId().equals(nodeId)); } public boolean isLeader(ClusterNode thisNode) { return members.last().getId().equals(thisNode.getId()); } } }