package netflix.ocelli.util; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observable.Operator; import rx.Observer; import rx.Subscriber; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func0; import rx.functions.Func1; import rx.functions.FuncN; import rx.subscriptions.Subscriptions; public class RxUtil { private static final Logger LOG = LoggerFactory.getLogger(RxUtil.class); private interface Action01<T> extends Action1<T>, Action0 {} /** * Increment a stateful counter outside the stream. * * {code * <pre> * observable * .doOnNext(RxUtil.increment(mycounter)) * </pre> * } * * @param metric * @return */ public static <T> Action01<T> increment(final AtomicLong metric) { return new Action01<T>() { @Override public void call(T t1) { metric.incrementAndGet(); } @Override public void call() { metric.incrementAndGet(); } }; } public static <T> Action01<T> increment(final AtomicInteger metric) { return new Action01<T>() { @Override public void call(T t1) { metric.incrementAndGet(); } @Override public void call() { metric.incrementAndGet(); } }; } /** * Decrement a stateful counter outside the stream. * * {code * <pre> * observable * .doOnNext(RxUtil.decrement(mycounter)) * </pre> * } * * @param metric * @return */ public static <T> Action01<T> decrement(final AtomicLong metric) { return new Action01<T>() { @Override public void call(T t1) { metric.decrementAndGet(); } @Override public void call() { metric.decrementAndGet(); } }; } /** * Trace each item emitted on the stream with a given label. * Will log the file and line where the trace occurs. * * {code * <pre> * observable * .doOnNext(RxUtil.trace("next: ")) * </pre> * } * * @param label */ public static <T> Action01<T> trace(String label) { final String caption = getSourceLabel(label); final AtomicLong counter = new AtomicLong(); return new Action01<T>() { @Override public void call(T t1) { LOG.trace("{} ({}) {}", caption, counter.incrementAndGet(), t1); } @Override public void call() { LOG.trace("{} ({}) {}", caption, counter.incrementAndGet()); } }; } /** * Log info line for each item emitted on the stream with a given label. * Will log the file and line where the trace occurs. * * {code * <pre> * observable * .doOnNext(RxUtil.info("next: ")) * </pre> * } * * @param label */ public static <T> Action01<T> info(String label) { final String caption = getSourceLabel(label); final AtomicLong counter = new AtomicLong(); return new Action01<T>() { @Override public void call(T t1) { LOG.info("{} ({}) {}", caption, counter.incrementAndGet(), t1); } @Override public void call() { LOG.info("{} ({})", caption, counter.incrementAndGet()); } }; } /** * Action to sleep in the middle of a pipeline. This is normally used in tests to * introduce an artifical delay. * @param timeout * @param units * @return */ public static <T> Action01<T> sleep(final long timeout, final TimeUnit units) { return new Action01<T>() { @Override public void call(T t1) { call(); } @Override public void call() { try { units.sleep(timeout); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } } }; } /** * Decrement a countdown latch for each item * * {code * <pre> * observable * .doOnNext(RxUtil.decrement(latch)) * </pre> * } */ public static <T> Action01<T> countdown(final CountDownLatch latch) { return new Action01<T>() { @Override public void call(T t1) { latch.countDown(); } @Override public void call() { latch.countDown(); } }; } /** * Log the request rate at the given interval. * * {code * <pre> * observable * .lift(RxUtil.rate("items per 10 seconds", 10, TimeUnit.SECONDS)) * </pre> * } * * @param label * @param interval * @param units */ public static String[] TIME_UNIT = {"ns", "us", "ms", "s", "m", "h", "d"}; public static <T> Operator<T, T> rate(final String label, final long interval, final TimeUnit units) { final String caption = getSourceLabel(label); return new Operator<T, T>() { @Override public Subscriber<? super T> call(final Subscriber<? super T> child) { final AtomicLong counter = new AtomicLong(); final String sUnits = (interval == 1) ? TIME_UNIT[units.ordinal()] : String.format("({} {})", interval, TIME_UNIT[units.ordinal()]); child.add( Observable.interval(interval, units) .subscribe(new Action1<Long>() { @Override public void call(Long t1) { LOG.info("{} {} / {}", caption, counter.getAndSet(0), sUnits); } })); return new Subscriber<T>(child) { @Override public void onCompleted() { if (!isUnsubscribed()) child.onCompleted(); } @Override public void onError(Throwable e) { if (!isUnsubscribed()) child.onError(e); } @Override public void onNext(T t) { counter.incrementAndGet(); if (!isUnsubscribed()) child.onNext(t); } }; } }; } /** * Log error line when an error occurs. * Will log the file and line where the trace occurs. * * {code * <pre> * observable * .doOnError(RxUtil.error("Stream broke")) * </pre> * } * * @param label */ public static Action1<Throwable> error(String label) { final String caption = getSourceLabel(label); final AtomicLong counter = new AtomicLong(); return new Action1<Throwable>() { @Override public void call(Throwable t1) { LOG.error("{} ({}) {}", caption, counter.incrementAndGet(), t1); } }; } /** * Log a warning line when an error occurs. * Will log the file and line where the trace occurs. * * {code * <pre> * observable * .doOnError(RxUtil.warn("Stream broke")) * </pre> * } * * @param label */ public static Action1<Throwable> warn(String label) { final String caption = getSourceLabel(label); final AtomicLong counter = new AtomicLong(); return new Action1<Throwable>() { @Override public void call(Throwable t1) { LOG.warn("{} ({}) {}", caption, counter.incrementAndGet(), t1); } }; } public static <T> Func1<List<T>, Boolean> listNotEmpty() { return new Func1<List<T>, Boolean>() { @Override public Boolean call(List<T> t1) { return !t1.isEmpty(); } }; } /** * Filter out any collection that is empty. * * {code * <pre> * observable * .filter(RxUtil.collectionNotEmpty()) * </pre> * } */ public static <T> Func1<Collection<T>, Boolean> collectionNotEmpty() { return new Func1<Collection<T>, Boolean>() { @Override public Boolean call(Collection<T> t1) { return !t1.isEmpty(); } }; } /** * Operator that acts as a pass through. Use this when you want the operator * to be interchangable with the default implementation being a single passthrough. * * {code * <pre> * Operator<T,T> customOperator = RxUtil.passthrough(); * observable * .lift(customOperator) * </pre> * } */ public static <T> Operator<T, T> passthrough() { return new Operator<T, T>() { @Override public Subscriber<? super T> call(final Subscriber<? super T> o) { return o; } }; } /** * Cache all items and emit a single LinkedHashSet with all data when onComplete is called * @return */ public static <T> Operator<Set<T>, T> toLinkedHashSet() { return new Operator<Set<T>, T>() { @Override public Subscriber<? super T> call(final Subscriber<? super Set<T>> o) { final Set<T> set = new LinkedHashSet<T>(); return new Subscriber<T>() { @Override public void onCompleted() { o.onNext(set); o.onCompleted(); } @Override public void onError(Throwable e) { o.onError(e); } @Override public void onNext(T t) { set.add(t); } }; } }; } private static String getSourceLabel(String label) { StackTraceElement[] stack = Thread.currentThread().getStackTrace(); StackTraceElement element = stack[3]; return "(" + element.getFileName() + ":" + element.getLineNumber() + ") " + label; } /** * Filter that returns true whenever an external state is true * {code * <pre> * final AtomicBoolean condition = new AtomicBoolean(); * * observable * .filter(RxUtil.isTrue(condition)) * </pre> * } * @param condition */ public static <T> Func1<T, Boolean> isTrue(final AtomicBoolean condition) { return new Func1<T, Boolean>() { @Override public Boolean call(T t1) { return condition.get(); } }; } /** * Filter that returns true whenever an external state is false * {code * <pre> * final AtomicBoolean condition = new AtomicBoolean(); * * observable * .filter(RxUtil.isTrue(condition)) * </pre> * } * @param condition */ public static <T> Func1<T, Boolean> isFalse(final AtomicBoolean condition) { return new Func1<T, Boolean>() { @Override public Boolean call(T t1) { return !condition.get(); } }; } /** * Filter that returns true whenever a CAS operation on an external static * AtomicBoolean succeeds * {code * <pre> * final AtomicBoolean condition = new AtomicBoolean(); * * observable * .filter(RxUtil.isTrue(condition)) * </pre> * } * @param condition */ public static Func1<? super Long, Boolean> compareAndSet(final AtomicBoolean condition, final boolean expect, final boolean value) { return new Func1<Long, Boolean>() { @Override public Boolean call(Long t1) { return condition.compareAndSet(expect, value); } }; } /** * Simple operation that sets an external condition for each emitted item * {code * <pre> * final AtomicBoolean condition = new AtomicBoolean(); * * observable * .doOnNext(RxUtil.set(condition, true)) * </pre> * } * @param condition * @param value * @return */ public static <T> Action01<T> set(final AtomicBoolean condition, final boolean value) { return new Action01<T>() { @Override public void call(T t1) { condition.set(value); } @Override public void call() { condition.set(value); } }; } /** * Filter that always returns a constant value. Use this to create a default filter * when the filter implementation is plugable. * * @param constant * @return */ public static <T> Func1<T, Boolean> constantFilter(final boolean constant) { return new Func1<T, Boolean>() { @Override public Boolean call(T t1) { return constant; } }; } /** * Observable factory to be used with {@link Observable.defer()} which will round robin * through a list of {@link Observable}'s so that each subscribe() returns the next * {@link Observable} in the list. * * @param sources * @return */ public static <T> Func0<Observable<T>> roundRobinObservableFactory(@SuppressWarnings("unchecked") final Observable<T> ... sources) { return new Func0<Observable<T>>() { final AtomicInteger count = new AtomicInteger(); @Override public Observable<T> call() { int index = count.getAndIncrement() % sources.length; return sources[index]; } }; } public static <T> Observable<Observable<T>> onSubscribeChooseNext(final Observable<T> ... sources) { return Observable.create(new OnSubscribe<Observable<T>>() { private AtomicInteger count = new AtomicInteger(); @Override public void call(Subscriber<? super Observable<T>> t1) { int index = count.getAndIncrement(); if (index < sources.length) { t1.onNext(sources[index]); } t1.onCompleted(); } }); } /** * Given a list of observables that emit a boolean condition AND all conditions whenever * any condition changes and emit the resulting condition when the final condition changes. * @param sources * @return */ public static Observable<Boolean> conditionAnder(List<Observable<Boolean>> sources) { return Observable.combineLatest(sources, new FuncN<Observable<Boolean>>() { @Override public Observable<Boolean> call(Object... args) { return Observable.from(args).cast(Boolean.class).firstOrDefault(true, new Func1<Boolean, Boolean>() { @Override public Boolean call(Boolean status) { return !status; } }); } }) .flatMap(new Func1<Observable<Boolean>, Observable<Boolean>>() { @Override public Observable<Boolean> call(Observable<Boolean> t1) { return t1; } }) .distinctUntilChanged(); } /** * Trace all parts of an observable's state and especially when * notifications are discarded due to being unsubscribed. This should * only used for debugging purposes. * @param label * @return */ public static <T> Operator<T, T> uberTracer(String label) { final String caption = getSourceLabel(label); return new Operator<T, T>() { @Override public Subscriber<? super T> call(final Subscriber<? super T> s) { s.add(Subscriptions.create(new Action0() { @Override public void call() { LOG.info("{} unsubscribing", caption); } })); return new Subscriber<T>(s) { private AtomicLong completedCounter = new AtomicLong(); private AtomicLong nextCounter = new AtomicLong(); private AtomicLong errorCounter = new AtomicLong(); @Override public void onCompleted() { if (!s.isUnsubscribed()) { s.onCompleted(); } else { LOG.info("{} ({}) Discarding onCompleted", caption, completedCounter.incrementAndGet()); } } @Override public void onError(Throwable e) { if (!s.isUnsubscribed()) { s.onCompleted(); } else { LOG.info("{} ({}) Discarding onError", caption, errorCounter.incrementAndGet()); } } @Override public void onNext(T t) { if (!s.isUnsubscribed()) { s.onNext(t); } else { LOG.info("{} ({}) Discarding onNext", caption, nextCounter.incrementAndGet()); } } }; } }; } /** * Utility to call an action when any event occurs regardless that event * @param action * @return */ public static <T> Observer<T> onAny(final Action0 action) { return new Observer<T>() { @Override public void onCompleted() { action.call(); } @Override public void onError(Throwable e) { action.call(); } @Override public void onNext(T t) { action.call(); } } ; } public static <T> Action01<T> acquire(final Semaphore sem) { return new Action01<T>() { @Override public void call(T t1) { call(); } @Override public void call() { try { sem.acquire(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; } public static <T> Action01<T> release(final Semaphore sem) { return new Action01<T>() { @Override public void call(T t1) { call(); } @Override public void call() { sem.release(); } }; } public static <T> Action1<T> set(final AtomicReference<T> ref) { return new Action1<T>() { @Override public void call(T t1) { ref.set(t1); } }; } }