package netflix.ocelli; import netflix.ocelli.InstanceQuarantiner.IncarnationFactory; import netflix.ocelli.loadbalancer.RoundRobinLoadBalancer; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observable.Transformer; import rx.Scheduler; import rx.Subscriber; import rx.Subscription; import rx.functions.Action1; import rx.functions.Func1; import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * The LoadBalancer tracks lifecycle of entities and selects the next best entity based on plugable * algorithms such as round robin, choice of two, random, weighted, weighted random, etc. * * The LoadBalancer provides two usage modes, LoadBalancer#next and LoadBalancer#toObservable. * LoadBalancer#next selects the next best entity from the list of known entities based on the * load balancing algorithm. LoadBalancer#toObservable() creates an Observable that whenever * subscribed to will emit a single next best entity. Use toObservable() to compose with * Rx retry operators. * * Use one of the fromXXX methods to begin setting up a load balancer builder. The builder begins * with a source of entities that can be augmented with different topologies and quarantine strategies. * Finally the load balancer is created by calling build() with the desired algorithm. * * @author elandau * * @param <T> */ public class LoadBalancer<T> { public static class Builder<T> { private final Observable<Instance<T>> source; Builder(Observable<Instance<T>> source) { this.source = source; } /** * Topology to limit the number of active instances from the previously provided source. A * topology will track lifecycle of all source provided entities but only send a subset of these * entities to the load balancer. * * @param topology * @return */ public Builder<T> withTopology(Transformer<Instance<T>, Instance<T>> topology) { return new Builder<T>(source.compose(topology)); } /** * The quaratiner creates a new incarnation of each entity while letting the entity manage its * own lifecycle that onCompletes whenever the entity fails. A separate lifecycle (Observable<Void>) * is tracked for each incarnation with all incarnation subject to the original entities membership * lifecycle. Determination failure is deferred to the entity itself. * * @param factory * @param backoffStrategy * @param scheduler * @return */ public Builder<T> withQuarantiner(IncarnationFactory<T> factory, DelayStrategy backoffStrategy, Scheduler scheduler) { return new Builder<T>(source.flatMap(InstanceQuarantiner.create(factory, backoffStrategy, scheduler))); } /** * The quaratiner creates a new incarnation of each entity while letting the entity manage its * own lifecycle that onCompletes whenever the entity fails. A separate lifecycle (Observable<Void>) * is tracked for each incarnation with all incarnation subject to the original entities membership * lifecycle. Determination failure is deferred to the entity itself. * * @param factory * @param backoffStrategy * @param scheduler * @return */ public Builder<T> withQuarantiner(IncarnationFactory<T> factory, DelayStrategy backoffStrategy) { return new Builder<T>(source.flatMap(InstanceQuarantiner.create(factory, backoffStrategy, Schedulers.io()))); } /** * Convert the client from one type to another. Note that topology or failure detection will * still occur on the previous type * @param converter * @return */ public <S> Builder<S> convertTo(Func1<Instance<T>, Instance<S>> converter) { return new Builder<S>(source.map(converter)); } /** * Construct the default LoadBalancer using the round robin load balancing strategy * @return */ public LoadBalancer<T> buildDefault() { return new LoadBalancer<T>(source.compose(InstanceCollector.<T>create()), RoundRobinLoadBalancer.<T>create()); } /** * Finally create a load balancer given a specified strategy such as RoundRobin or ChoiceOfTwo * @param strategy * @return */ public LoadBalancer<T> build(LoadBalancerStrategy<T> strategy) { return new LoadBalancer<T>(source.compose(InstanceCollector.<T>create()), strategy); } /** * Finally create a load balancer given a specified strategy such as RoundRobin or ChoiceOfTwo * @param strategy * @return */ public LoadBalancer<T> build(LoadBalancerStrategy<T> strategy, InstanceCollector<T> instanceCollector) { return new LoadBalancer<T>(source.compose(instanceCollector), strategy); } } /** * Start the builder from a stream of Instance<T> where each emitted item represents an added * instances and the instance's Instance#getLifecycle() onCompletes when the instance is removed. * * The source can be managed manually via {@link InstanceManager} or may be tied directly to a hot * Observable from a host registry service such as Eureka. Note that the source may itself be a * composed Observable that includes transformations from one type to another. * * @param source * @return */ public static <T> Builder<T> fromSource(Observable<Instance<T>> source) { return new Builder<T>(source); } /** * Construct a load balancer builder from a stream of client snapshots. Note that * T must implement hashCode() and equals() so that a proper delta may determined * between successive snapshots. * * @param source * @return */ public static <T> Builder<T> fromSnapshotSource(Observable<List<T>> source) { return new Builder<T>(source.compose(new SnapshotToInstance<T>())); } /** * Construct a load balancer builder from a fixed list of clients * @param clients * @return */ public static <T> Builder<T> fromFixedSource(List<T> clients) { return fromSnapshotSource(Observable.just(clients)); } private final static Subscription IDLE_SUBSCRIPTION = Subscriptions.empty(); private final static Subscription SHUTDOWN_SUBSCRIPTION = Subscriptions.empty(); private final AtomicReference<Subscription> subscription; private volatile List<T> cache; private final AtomicBoolean isSubscribed = new AtomicBoolean(false); private final Observable<List<T>> source; private final LoadBalancerStrategy<T> algo; // Visible for testing only static <T> LoadBalancer<T> create(Observable<List<T>> source, LoadBalancerStrategy<T> algo) { return new LoadBalancer<T>(source, algo); } // Visible for testing only LoadBalancer(final Observable<List<T>> source, final LoadBalancerStrategy<T> algo) { this.source = source; this.subscription = new AtomicReference<Subscription>(IDLE_SUBSCRIPTION); this.cache = cache; this.algo = algo; } public Observable<T> toObservable() { return Observable.create(new OnSubscribe<T>() { @Override public void call(Subscriber<? super T> s) { try { s.onNext(next()); s.onCompleted(); } catch (Exception e) { s.onError(e); } } }); } /** * Select the next best T from the source. Will auto-subscribe to the source on the first * call to next(). shutdown() must be called to unsubscribe() from the source once the load * balancer is no longer used. * * @return * @throws NoSuchElementException */ public T next() throws NoSuchElementException { // Auto-subscribe if (isSubscribed.compareAndSet(false, true)) { Subscription s = source.subscribe(new Action1<List<T>>() { @Override public void call(List<T> t1) { cache = t1; } }); // Prevent subscription after shutdown if (!subscription.compareAndSet(IDLE_SUBSCRIPTION, s)) { s.unsubscribe(); } } List<T> latest = cache; if (latest == null) { throw new NoSuchElementException(); } return algo.choose(latest); } /** * Shut down the source subscription. This LoadBalancer may no longer be used * after shutdown is called. */ public void shutdown() { Subscription s = subscription.getAndSet(SHUTDOWN_SUBSCRIPTION); s.unsubscribe(); cache = null; } }