package netflix.ocelli.topologies; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import netflix.ocelli.CloseableInstance; import netflix.ocelli.Instance; import netflix.ocelli.InstanceToNotification; import netflix.ocelli.InstanceToNotification.InstanceNotification; import rx.Observable; import rx.Observable.Transformer; import rx.Scheduler; import rx.functions.Action0; import rx.functions.Func1; import rx.functions.Func2; import rx.schedulers.Schedulers; /** * The ring topology uses consistent hashing to arrange all hosts in a predictable ring * topology such that each client instance will be located in a uniformly distributed * fashion around the ring. The client will then target the next N hosts after its location. * * This type of topology ensures that each client instance communicates with a subset of * hosts in such a manner that the overall load shall be evenly distributed. * * @author elandau * * @param <T> */ public class RingTopology<K extends Comparable<K>, T> implements Transformer<Instance<T>, Instance<T>> { private final Func1<Integer, Integer> countFunc; private final Func1<T, K> keyFunc; private final Entry local; class Entry implements Comparable<Entry> { final K key; final T value; CloseableInstance<T> instance; public Entry(K key) { this(key, null); } public Entry(K key, T value) { this.key = key; this.value = value; } @Override public int compareTo(Entry o) { return key.compareTo(o.key); } public void setInstance(CloseableInstance<T> instance) { if (this.instance != null) { this.instance.close(); } this.instance = instance; } public void closeInstance() { setInstance(null); } public String toString() { return key.toString(); } } class State { // Complete hash ordered list of all existing instances final List<Entry> ring = new ArrayList<Entry>(); // Lookup of all entries in the active list final Map<K, Entry> active = new HashMap<K, Entry>(); State() { ring.add(local); } Observable<Instance<T>> update() { Collections.sort(ring); // Get the starting position in the ring and number of entries // that should be active. int pos = Collections.binarySearch(ring, local) + 1; int count = Math.min(ring.size() - 1, countFunc.call(ring.size() - 1)); // Determine the current 'active' set Set<Entry> current = new HashSet<Entry>(); for (int i = 0; i < count; i++) { current.add(ring.get((pos + i) % ring.size())); } // Determine Entries that have either been added or removed Set<Entry> added = new HashSet<Entry>(current); added.removeAll(active.values()); final Set<Entry> removed = new HashSet<Entry>(active.values()); removed.removeAll(current); // Update the active list for (Entry entry : added) { active.put(entry.key, entry); } return Observable // New instance will be added immediately .from(added) .map(new Func1<Entry, Instance<T>>() { @Override public Instance<T> call(Entry entry) { CloseableInstance<T> instance = CloseableInstance.from(entry.value); entry.setInstance(instance); return instance; } } ) .doOnCompleted(new Action0() { @Override public void call() { for (Entry entry : removed) { entry.closeInstance(); active.remove(entry.key); } } }); } public void add(Instance<T> value) { ring.add(new Entry(keyFunc.call(value.getValue()), value.getValue())); } public void remove(Instance<T> value) { K key = keyFunc.call(value.getValue()); int pos = Collections.binarySearch(ring, new Entry(key)); if (pos >= 0) { ring.remove(pos).closeInstance(); active.remove(key); } } } public static <K extends Comparable<K>, T> RingTopology<K, T> create(final K localKey, final Func1<T, K> keyFunc, Func1<Integer, Integer> countFunc) { return new RingTopology<K, T>(localKey, keyFunc, countFunc); } public static <K extends Comparable<K>, T> RingTopology<K, T> create(final K localKey, final Func1<T, K> keyFunc, Func1<Integer, Integer> countFunc, Scheduler scheduler) { return new RingTopology<K, T>(localKey, keyFunc, countFunc, scheduler); } public RingTopology(final K localKey, final Func1<T, K> keyFunc, Func1<Integer, Integer> countFunc) { this(localKey, keyFunc, countFunc, Schedulers.computation()); } // Visible for testing public RingTopology(final K localKey, final Func1<T, K> keyFunc, Func1<Integer, Integer> countFunc, Scheduler scheduler) { this.local = new Entry(localKey); this.countFunc = countFunc; this.keyFunc = keyFunc; } @Override public Observable<Instance<T>> call(Observable<Instance<T>> o) { return o .flatMap(InstanceToNotification.<T>create()) .scan(new State(), new Func2<State, InstanceNotification<T>, State>() { @Override public State call(State state, InstanceNotification<T> instance) { switch (instance.getKind()) { case OnAdd: state.add(instance.getInstance()); break; case OnRemove: state.remove(instance.getInstance()); break; } return state; } }) .concatMap(new Func1<State, Observable<Instance<T>>>() { @Override public Observable<Instance<T>> call(State state) { return state.update(); } }); } }