/******************************************************************************* * Copyright (c) 2016 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.v2; import java.io.IOException; import java.time.Duration; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import org.eclipse.core.runtime.OperationCanceledException; import org.reactivestreams.Publisher; import org.springframework.ide.eclipse.boot.dash.util.CancelationTokens.CancelationToken; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; /** * @author Kris De Volder */ public class ReactorUtils { private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(45); // reflects default timeout of Mono.block in reactor 2.x. public static boolean DUMP_STACK_ON_TIMEOUT = false; /** * Convert a {@link CancelationToken} into a Mono that raises * an {@link OperationCanceledException} when the token is canceled. */ public static <T> Mono<T> toMono(CancelationToken cancelToken) { return Mono.delay(Duration.ofSeconds(1)) .then((ping) -> cancelToken.isCanceled() ? Mono.<T>error(new OperationCanceledException()) : Mono.empty() ) .repeatWhenEmpty((x) -> x); } /** * Similar to Mono.get but logs a more traceable version of the exception to Eclipse's error * log before 'rethrowing' it. * <p> * This is useful because the actual exception is pretty hard to trace. It doesn't even 'point' * to the line where 'get' was called. */ public static <T> T get(Mono<T> mono) throws Exception { try { return mono.block(DEFAULT_TIMEOUT); } catch (Exception e) { dumpStacks(); throw new IOException(e); } } /** * Similar to Mono.get but logs a more traceable version of the exception to Eclipse's error * log before 'rethrowing' it. * <p> * This is useful because the actual exception is pretty hard to trace. It doesn't even 'point' * to the line where 'get' was called. */ public static <T> T get(Duration timeout, CancelationToken cancelationToken, Mono<T> mono) throws Exception { try { return Mono.first(mono, toMono(cancelationToken)) .otherwise(errorFilter(cancelationToken)) .block(timeout); } catch (Exception e) { dumpStacks(); throw new IOException(e); } } public static <T> List<T> get(Duration t, Mono<List<T>> m) throws IOException { try { return m.block(t); } catch (Exception e) { dumpStacks(); throw new IOException(e); } } private static void dumpStacks() { if (DUMP_STACK_ON_TIMEOUT) { System.out.println(getStackDumps().toString()); } } /** * A 'filter' to use as a Mono.otherwise hanlder. It transforms any exception into {@link OperationCanceledException} * when cancelationToken has been canceled. */ private static <T> Function<Throwable, Mono<T>> errorFilter(CancelationToken cancelationToken) { return (Throwable e) -> cancelationToken.isCanceled()?Mono.error(new OperationCanceledException()):Mono.error(e); } /** * Deprecated because this is really the same as Mono.justOrEmpty, so use that instead. */ @Deprecated public static <T> Mono<T> just(T it) { return it == null ? Mono.empty() : Mono.just(it); } /** * @return A function that can be passed to Mono.otherwise to convert a specific exception type into * Mono.empty(). */ public static <T> Function<Throwable, Mono<T>> suppressException(Class<? extends Throwable> exceptionType) { return (Throwable caught) -> { if (exceptionType.isAssignableFrom(caught.getClass())) { return Mono.empty(); } else { return Mono.error(caught); } }; } /** * Build a Mono<Void> that executes a given number of Mono<Void> one after the * other. */ @SafeVarargs public static Mono<Void> sequence(Mono<Void>... tasks) { Mono<Void> seq = Mono.empty(); for (Mono<Void> t : tasks) { seq = seq.then(t); } return seq; } /** * Execute a bunch of mono in parallel. All monos are executed to completion (rather than canceled early * when one of them fails) * <p> * When at least one operation has failed then, upon completion or failure of the last Mono we guarantee that at least * one of the exceptions is propagated. */ public static Mono<Void> safeMerge(Flux<Mono<Void>> operations, int concurrency) { AtomicReference<Throwable> failure = new AtomicReference<>(null); return Flux.merge( operations .map((Mono<Void> op) -> { return op.otherwise((e) -> { failure.compareAndSet(null, e); return Mono.empty(); }); }), concurrency //limit concurrency otherwise troubles (flooding/choking request broker?) ) .then(() -> { Throwable error = failure.get(); if (error!=null) { return Mono.error(error); } else { return Mono.empty(); } }); } /** * Attach a timestamp to each element in a Stream */ public static <T> Flux<Tuple2<T,Long>> timestamp(Flux<T> stream) { return stream.map((e) -> Tuples.of(e, System.currentTimeMillis())); } /** * Sorts the elements in a flux in a moving time window. I.e. this assumes element order may be * scrambled but the scrambling has a certain 'time localilty' to it. So we only need to consider * sorting of elements that arrive 'close to eachother'. * <p> * WARNING: The returned flux is intended for a single subscriber. It only maintains a * single buffer for sorting stream elements. This buffer is consumed when elements * are released to any subscriber. Therefore if one subscriber received a element it is gone * from the buffer and will not be delivered to the other subscribers. * * @param stream The stream to be sorted * @param comparator Compare function to sort with * @param bufferTime The 'window' of time beyond which we don't need to compare elements. */ public static <T> Flux<T> sort(Flux<T> stream, Comparator<T> comparator, Duration bufferTime) { class SorterAccumulator { final PriorityQueue<Tuple2<T, Long>> holdingPen = new PriorityQueue<>((Tuple2<T, Long> o1, Tuple2<T, Long> o2) -> { return comparator.compare(o1.getT1(), o2.getT1()); }); final Flux<T> released = Flux.fromIterable(() -> new Iterator<T>() { @Override public boolean hasNext() { Tuple2<T, Long> nxt; synchronized (holdingPen) { nxt = holdingPen.peek(); } return nxt!=null && isOldEnough(nxt); } private boolean isOldEnough(Tuple2<T, Long> nxt) { long age = System.currentTimeMillis() - nxt.getT2(); return age > bufferTime.toMillis(); } @Override public T next() { synchronized (holdingPen) { return holdingPen.remove().getT1(); } } }); public SorterAccumulator next(Flux<Tuple2<T, Long>> window) { window.subscribe((e) -> { synchronized (holdingPen) { holdingPen.add(e); } }); return this; } public Flux<T> getReleased() { return released; } public Publisher<? extends T> drain() { return Flux.fromIterable(holdingPen) .map(Tuple2::getT1); } } SorterAccumulator sorter = new SorterAccumulator(); return timestamp(stream) .window(bufferTime) .scan(sorter, SorterAccumulator::next) .concatMap(SorterAccumulator::getReleased) .concatWith(sorter.drain()); } protected static StringBuffer getStackDumps() { StringBuffer sb = new StringBuffer(); Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces(); for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) { sb.append(entry.getKey().toString()); sb.append("\n"); for (StackTraceElement element : entry.getValue()) { sb.append(" "); sb.append(element.toString()); sb.append("\n"); } sb.append("\n"); } return sb; } /** * Connect a mono to a CompletableFuture so that the result of the mono * can be retrieved from the {@link CompletableFuture} by calling it's 'get' * method. */ public static <T> void completeWith(CompletableFuture<T> future, Mono<T> mono) { mono.doOnNext((T v) -> { future.complete(v); }) .doOnError((Throwable e) -> { future.completeExceptionally(e); }) .subscribeOn(Schedulers.elastic()) .subscribe(); } }