/* * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. * * 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 reactor.guide; import java.io.IOException; import java.time.Duration; import java.time.LocalTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.reactivestreams.Subscription; import reactor.core.Disposable; import reactor.core.Exceptions; import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.ConnectableFlux; import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.core.publisher.SignalType; import reactor.core.publisher.UnicastProcessor; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import reactor.test.scheduler.VirtualTimeScheduler; import reactor.util.function.Tuple2; import static org.assertj.core.api.Assertions.assertThat; /** * Tests mirroring snippets from the reference documentation. * * @author Stephane Maldini * @author Simon Baslé */ public class GuideTests { @Test public void introFutureHell() { CompletableFuture<List<String>> ids = ifhIds(); // <1> CompletableFuture<List<String>> result = ids.thenComposeAsync(l -> { // <2> Stream<CompletableFuture<String>> zip = l.stream().map(i -> { // <3> CompletableFuture<String> nameTask = ifhName(i); // <4> CompletableFuture<Integer> statTask = ifhStat(i); // <5> return nameTask.thenCombineAsync(statTask, (name, stat) -> "Name " + name + " has stats " + stat); // <6> }); List<CompletableFuture<String>> combinationList = zip.collect(Collectors.toList()); // <7> CompletableFuture<String>[] combinationArray = combinationList.toArray(new CompletableFuture[combinationList.size()]); CompletableFuture<Void> allDone = CompletableFuture.allOf(combinationArray); // <8> return allDone.thenApply(v -> combinationList.stream() .map(CompletableFuture::join) // <9> .collect(Collectors.toList())); }); List<String> results = result.join(); // <10> assertThat(results).contains( "Name NameJoe has stats 103", "Name NameBart has stats 104", "Name NameHenry has stats 105", "Name NameNicole has stats 106", "Name NameABSLAJNFOAJNFOANFANSF has stats 121"); } @Test public void introFutureHellReactorVersion() { Flux<String> ids = ifhrIds(); // <1> Flux<String> combinations = ids.flatMap(id -> { // <2> Mono<String> nameTask = ifhrName(id); // <3> Mono<Integer> statTask = ifhrStat(id); // <4> return nameTask.and(statTask, // <5> (name, stat) -> "Name " + name + " has stats " + stat); }); Mono<List<String>> result = combinations.collectList(); // <6> List<String> results = result.block(); // <7> assertThat(results).containsExactly( // <8> "Name NameJoe has stats 103", "Name NameBart has stats 104", "Name NameHenry has stats 105", "Name NameNicole has stats 106", "Name NameABSLAJNFOAJNFOANFANSF has stats 121" ); } private CompletableFuture<String> ifhName(String id) { CompletableFuture<String> f = new CompletableFuture<>(); f.complete("Name" + id); return f; } private CompletableFuture<Integer> ifhStat(String id) { CompletableFuture<Integer> f = new CompletableFuture<>(); f.complete(id.length() + 100); return f; } private CompletableFuture<List<String>> ifhIds() { CompletableFuture<List<String>> ids = new CompletableFuture<>(); ids.complete(Arrays.asList("Joe", "Bart", "Henry", "Nicole", "ABSLAJNFOAJNFOANFANSF")); return ids; } private Flux<String> ifhrIds() { return Flux.just("Joe", "Bart", "Henry", "Nicole", "ABSLAJNFOAJNFOANFANSF"); } private Mono<String> ifhrName(String id) { return Mono.just("Name" + id); } private Mono<Integer> ifhrStat(String id) { return Mono.just(id.length() + 100); } @Test public void advancedCompose() { Function<Flux<String>, Flux<String>> filterAndMap = f -> f.filter(color -> !color.equals("orange")) .map(String::toUpperCase); Flux.fromIterable(Arrays.asList("blue", "green", "orange", "purple")) .doOnNext(System.out::println) .transform(filterAndMap) .subscribe(d -> System.out.println("Subscriber to Transformed MapAndFilter: "+d)); } @Test public void advancedTransform() { AtomicInteger ai = new AtomicInteger(); Function<Flux<String>, Flux<String>> filterAndMap = f -> { if (ai.incrementAndGet() == 1) { return f.filter(color -> !color.equals("orange")) .map(String::toUpperCase); } return f.filter(color -> !color.equals("purple")) .map(String::toUpperCase); }; Flux<String> composedFlux = Flux.fromIterable(Arrays.asList("blue", "green", "orange", "purple")) .doOnNext(System.out::println) .compose(filterAndMap); composedFlux.subscribe(d -> System.out.println("Subscriber 1 to Composed MapAndFilter :"+d)); composedFlux.subscribe(d -> System.out.println("Subscriber 2 to Composed MapAndFilter: "+d)); } @Test public void advancedCold() { Flux<String> source = Flux.fromIterable(Arrays.asList("blue", "green", "orange", "purple")) .doOnNext(System.out::println) .filter(s -> s.startsWith("o")) .map(String::toUpperCase); source.subscribe(d -> System.out.println("Subscriber 1: "+d)); source.subscribe(d -> System.out.println("Subscriber 2: "+d)); } @Test public void advancedHot() { UnicastProcessor<String> hotSource = UnicastProcessor.create(); Flux<String> hotFlux = hotSource.publish() .autoConnect() .map(String::toUpperCase); hotFlux.subscribe(d -> System.out.println("Subscriber 1 to Hot Source: "+d)); hotSource.onNext("blue"); hotSource.onNext("green"); hotFlux.subscribe(d -> System.out.println("Subscriber 2 to Hot Source: "+d)); hotSource.onNext("orange"); hotSource.onNext("purple"); hotSource.onComplete(); } @Test public void advancedConnectable() throws InterruptedException { Flux<Integer> source = Flux.range(1, 3) .doOnSubscribe(s -> System.out.println("subscribed to source")); ConnectableFlux<Integer> co = source.publish(); co.subscribe(System.out::println, e -> {}, () -> {}); co.subscribe(System.out::println, e -> {}, () -> {}); System.out.println("done subscribing"); Thread.sleep(500); System.out.println("will now connect"); co.connect(); } @Test public void advancedConnectableAutoConnect() throws InterruptedException { Flux<Integer> source = Flux.range(1, 3) .doOnSubscribe(s -> System.out.println("subscribed to source")); Flux<Integer> autoCo = source.publish().autoConnect(2); autoCo.subscribe(System.out::println, e -> {}, () -> {}); System.out.println("subscribed first"); Thread.sleep(500); System.out.println("subscribing second"); autoCo.subscribe(System.out::println, e -> {}, () -> {}); } @Test public void advancedParallelJustDivided() { Flux.range(1, 10) .parallel(2) //<1> .subscribe(i -> System.out.println(Thread.currentThread().getName() + " -> " + i)); } @Test public void advancedParallelParallelized() { Flux.range(1, 10) .parallel(2) .runOn(Schedulers.parallel()) .subscribe(i -> System.out.println(Thread.currentThread().getName() + " -> " + i)); } private Flux<String> someStringSource() { return Flux.just("foo", "bar", "baz").hide(); } @Test public void baseSubscriberFineTuneBackpressure() { Flux<String> source = someStringSource(); source.map(String::toUpperCase) .subscribe(new BaseSubscriber<String>() { // <1> @Override protected void hookOnSubscribe(Subscription subscription) { // <2> request(1); // <3> } @Override protected void hookOnNext(String value) { request(1); // <4> } //<5> }); } private String doSomethingDangerous(long i) { if (i < 5) return String.valueOf(i); throw new IllegalArgumentException("boom" + i); } private String doSecondTransform(String i) { return "item" + i; } @Test public void errorHandlingOnError() { Flux<String> s = Flux.range(1, 10) .map(v -> doSomethingDangerous(v)) // <1> .map(v -> doSecondTransform(v)); // <2> s.subscribe(value -> System.out.println("RECEIVED " + value), // <3> error -> System.err.println("CAUGHT " + error) // <4> ); StepVerifier.create(s) .expectNext("item1") .expectNext("item2") .expectNext("item3") .expectNext("item4") .verifyErrorMessage("boom5"); } @Test public void errorHandlingTryCatch() { try { for (int i = 1; i < 11; i++) { String v1 = doSomethingDangerous(i); // <1> String v2 = doSecondTransform(v1); // <2> System.out.println("RECEIVED " + v2); } } catch (Throwable t) { System.err.println("CAUGHT " + t); // <3> } } @Test public void errorHandlingReturn() { Flux<String> flux = Flux.just(10) .map(this::doSomethingDangerous) .onErrorReturn("RECOVERED"); StepVerifier.create(flux) .expectNext("RECOVERED") .verifyComplete(); } @Test public void errorHandlingReturnFilter() { Flux<String> flux = Flux.just(10) .map(this::doSomethingDangerous) .onErrorReturn(e -> e.getMessage().equals("boom10"), "recovered10"); StepVerifier.create(flux) .expectNext("recovered10") .verifyComplete(); flux = Flux.just(9) .map(this::doSomethingDangerous) .onErrorReturn(e -> e.getMessage().equals("boom10"), "recovered10"); StepVerifier.create(flux) .verifyErrorMessage("boom9"); } private Flux<String> callExternalService(String key) { if (key.equals("key2")) return Flux.error(new IllegalStateException("boom")); if (key.startsWith("timeout")) return Flux.error(new TimeoutException()); if (key.startsWith("unknown")) return Flux.error(new UnknownKeyException()); return Flux.just(key.replace("key", "value")); } private Flux<String> getFromCache(String key) { return Flux.just("outdated" + key); } @Test public void errorHandlingOnErrorResume() { Flux<String> flux = Flux.just("key1", "key2") .flatMap(k -> callExternalService(k) // <1> .onErrorResume(e -> getFromCache(k)) // <2> ); StepVerifier.create(flux) .expectNext("value1", "outdatedkey2") .verifyComplete(); } private class UnknownKeyException extends RuntimeException { } private Flux<String> registerNewEntry(String key, String value) { return Flux.just(key + "=" + value); } @Test public void errorHandlingOnErrorResumeDependingOnError() { Flux<String> flux = Flux.just("timeout1", "unknown", "key2") .flatMap(k -> callExternalService(k) .onErrorResume(error -> { // <1> if (error instanceof TimeoutException) // <2> return getFromCache(k); else if (error instanceof UnknownKeyException) // <3> return registerNewEntry(k, "DEFAULT"); else return Flux.error(error); // <4> }) ); StepVerifier.create(flux) .expectNext("outdatedtimeout1") .expectNext("unknown=DEFAULT") .verifyErrorMessage("boom"); } private class BusinessException extends RuntimeException { public BusinessException(String message, Throwable cause) { super(message, cause); } } @Test public void errorHandlingRethrow1() { Flux<String> flux = Flux.just("timeout1") .flatMap(k -> callExternalService(k) .onErrorResume(original -> Flux.error( new BusinessException("oops, SLA exceeded", original)) ) ); StepVerifier.create(flux) .verifyErrorMatches(e -> e instanceof BusinessException && e.getMessage().equals("oops, SLA exceeded") && e.getCause() instanceof TimeoutException); } @Test public void errorHandlingRethrow2() { Flux<String> flux = Flux.just("timeout1") .flatMap(k -> callExternalService(k) .onErrorMap(original -> new BusinessException("oops, SLA exceeded", original)) ); StepVerifier.create(flux) .verifyErrorMatches(e -> e instanceof BusinessException && e.getMessage().equals("oops, SLA exceeded") && e.getCause() instanceof TimeoutException); } private void log(String s) { System.out.println(s); } @Test public void errorHandlingSideEffect() { LongAdder failureStat = new LongAdder(); Flux<String> flux = Flux.just("unknown") .flatMap(k -> callExternalService(k) // <1> .doOnError(e -> { failureStat.increment(); log("uh oh, falling back, service failed for key " + k); // <2> }) .onErrorResume(e -> getFromCache(k)) // <3> ); StepVerifier.create(flux) .expectNext("outdatedunknown") .verifyComplete(); assertThat(failureStat.intValue()).isEqualTo(1); } @Test public void errorHandlingUsing() { AtomicBoolean isDisposed = new AtomicBoolean(); Disposable disposableInstance = new Disposable() { @Override public void dispose() { isDisposed.set(true); // <4> } @Override public String toString() { return "DISPOSABLE"; } }; Flux<String> flux = Flux.using( () -> disposableInstance, // <1> disposable -> Flux.just(disposable.toString()), // <2> Disposable::dispose // <3> ); StepVerifier.create(flux) .expectNext("DISPOSABLE") .verifyComplete(); assertThat(isDisposed.get()).isTrue(); } @Test public void errorHandlingDoFinally() { LongAdder statsCancel = new LongAdder(); // <1> Flux<String> flux = Flux.just("foo", "bar") .doFinally(type -> { if (type == SignalType.CANCEL) // <2> statsCancel.increment(); // <3> }) .take(1); // <4> StepVerifier.create(flux) .expectNext("foo") .verifyComplete(); assertThat(statsCancel.intValue()).isEqualTo(1); } @Test public void errorHandlingIntervalMillisNotContinued() throws InterruptedException { VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); VirtualTimeScheduler.set(virtualTimeScheduler); Flux<String> flux = Flux.interval(Duration.ofMillis(250)) .map(input -> { if (input < 3) return "tick " + input; throw new RuntimeException("boom"); }) .onErrorReturn("Uh oh"); flux.subscribe(System.out::println); //Thread.sleep(2100); // <1> virtualTimeScheduler.advanceTimeBy(Duration.ofHours(1)); StepVerifier.withVirtualTime(() -> flux, () -> virtualTimeScheduler, Long.MAX_VALUE) .thenAwait(Duration.ofSeconds(3)) .expectNext("tick 0") .expectNext("tick 1") .expectNext("tick 2") .expectNext("Uh oh") .verifyComplete(); } @Test public void errorHandlingIntervalMillisRetried() throws InterruptedException { VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); VirtualTimeScheduler.set(virtualTimeScheduler); Flux<Tuple2<Long,String>> flux = Flux.interval(Duration.ofMillis(250)) .map(input -> { if (input < 3) return "tick " + input; throw new RuntimeException("boom"); }) .elapsed() // <1> .retry(1); flux.subscribe(System.out::println, System.err::println); // <2> //Thread.sleep(2100); // <3> virtualTimeScheduler.advanceTimeBy(Duration.ofHours(1)); StepVerifier.withVirtualTime(() -> flux, () -> virtualTimeScheduler, Long.MAX_VALUE) .thenAwait(Duration.ofSeconds(3)) .expectNextMatches(t -> t.getT2().equals("tick 0")) .expectNextMatches(t -> t.getT2().equals("tick 1")) .expectNextMatches(t -> t.getT2().equals("tick 2")) .expectNextMatches(t -> t.getT2().equals("tick 0")) .expectNextMatches(t -> t.getT2().equals("tick 1")) .expectNextMatches(t -> t.getT2().equals("tick 2")) .verifyErrorMessage("boom"); } @Test public void errorHandlingRetryWhenApproximateRetry() { Flux<String> flux = Flux.<String>error(new IllegalArgumentException()) // <1> .doOnError(System.out::println) // <2> .retryWhen(companion -> companion.take(3)); // <3> StepVerifier.create(flux) .verifyComplete(); StepVerifier.create(Flux.<String>error(new IllegalArgumentException()).retry(3)) .verifyError(); } @Test public void errorHandlingRetryWhenEquatesRetry() { Flux<String> flux = Flux.<String>error(new IllegalArgumentException()) .retryWhen(companion -> companion .zipWith(Flux.range(1, 4), (error, index) -> { // <1> if (index < 4) return index; // <2> else throw Exceptions.propagate(error); // <3> }) ); StepVerifier.create(flux) .verifyError(IllegalArgumentException.class); StepVerifier.create(Flux.<String>error(new IllegalArgumentException()).retry(3)) .verifyError(); } @Test public void errorHandlingRetryWhenExponential() { Flux<String> flux = Flux.<String>error(new IllegalArgumentException()) .retryWhen(companion -> companion .doOnNext(s -> System.out.println(s + " at " + LocalTime.now())) // <1> .zipWith(Flux.range(1, 4), (error, index) -> { // <2> if (index < 4) return index; else throw Exceptions.propagate(error); }) .flatMap(index -> Mono.delay(Duration.ofMillis(index * 100))) // <3> .doOnNext(s -> System.out.println("retried at " + LocalTime.now())) // <4> ); StepVerifier.create(flux) .verifyError(IllegalArgumentException.class); } public String convert(int i) throws IOException { if (i > 3) { throw new IOException("boom " + i); } return "OK " + i; } @Test public void errorHandlingPropagateUnwrap() { Flux<String> converted = Flux .range(1, 10) .map(i -> { try { return convert(i); } catch (IOException e) { throw Exceptions.propagate(e); } }); converted.subscribe( v -> System.out.println("RECEIVED: " + v), e -> { if (Exceptions.unwrap(e) instanceof IOException) { System.out.println("Something bad happened with I/O"); } else { System.out.println("Something bad happened"); } } ); StepVerifier.create(converted) .expectNext("OK 1") .expectNext("OK 2") .expectNext("OK 3") .verifyErrorMessage("boom 4"); } @Test public void producingGenerate() { Flux<String> flux = Flux.generate( () -> 0, // <1> (state, sink) -> { sink.next("3 x " + state + " = " + 3*state); // <2> if (state == 10) sink.complete(); // <3> return state + 1; // <4> }); StepVerifier.create(flux) .expectNext("3 x 0 = 0") .expectNext("3 x 1 = 3") .expectNext("3 x 2 = 6") .expectNext("3 x 3 = 9") .expectNext("3 x 4 = 12") .expectNext("3 x 5 = 15") .expectNext("3 x 6 = 18") .expectNext("3 x 7 = 21") .expectNext("3 x 8 = 24") .expectNext("3 x 9 = 27") .expectNext("3 x 10 = 30") .verifyComplete(); } @Test public void producingGenerateMutableState() { Flux<String> flux = Flux.generate( AtomicLong::new, // <1> (state, sink) -> { long i = state.getAndIncrement(); // <2> sink.next("3 x " + i + " = " + 3*i); if (i == 10) sink.complete(); return state; // <3> }); StepVerifier.create(flux) .expectNext("3 x 0 = 0") .expectNext("3 x 1 = 3") .expectNext("3 x 2 = 6") .expectNext("3 x 3 = 9") .expectNext("3 x 4 = 12") .expectNext("3 x 5 = 15") .expectNext("3 x 6 = 18") .expectNext("3 x 7 = 21") .expectNext("3 x 8 = 24") .expectNext("3 x 9 = 27") .expectNext("3 x 10 = 30") .verifyComplete(); } interface MyEventListener<T> { void onDataChunk(List<T> chunk); void processComplete(); } interface MyEventProcessor { void register(MyEventListener<String> eventListener); void dataChunk(String... values); void processComplete(); } private MyEventProcessor myEventProcessor = new MyEventProcessor() { private MyEventListener<String> eventListener; private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); @Override public void register(MyEventListener<String> eventListener) { this.eventListener = eventListener; } @Override public void dataChunk(String... values) { executor.schedule(() -> eventListener.onDataChunk(Arrays.asList(values)), 500, TimeUnit.MILLISECONDS); } @Override public void processComplete() { executor.schedule(() -> eventListener.processComplete(), 500, TimeUnit.MILLISECONDS); } }; @Test public void producingCreate() { Flux<String> bridge = Flux.create(sink -> { myEventProcessor.register( // <4> new MyEventListener<String>() { // <1> public void onDataChunk(List<String> chunk) { for(String s : chunk) { sink.next(s); // <2> } } public void processComplete() { sink.complete(); // <3> } }); }); StepVerifier.withVirtualTime(() -> bridge) .expectSubscription() .expectNoEvent(Duration.ofSeconds(10)) .then(() -> myEventProcessor.dataChunk("foo", "bar", "baz")) .expectNext("foo", "bar", "baz") .expectNoEvent(Duration.ofSeconds(10)) .then(() -> myEventProcessor.processComplete()) .verifyComplete(); } public String alphabet(int letterNumber) { if (letterNumber < 1 || letterNumber > 26) { return null; } int letterIndexAscii = 'A' + letterNumber - 1; return "" + (char) letterIndexAscii; } @Test public void producingHandle() { Flux<String> alphabet = Flux.just(-1, 30, 13, 9, 20) .handle((i, sink) -> { String letter = alphabet(i); // <1> if (letter != null) // <2> sink.next(letter); // <3> }); alphabet.subscribe(System.out::println); StepVerifier.create(alphabet) .expectNext("M", "I", "T") .verifyComplete(); } private Flux<String> urls() { return Flux.range(1, 5) .map(i -> "http://mysite.io/quote/" + i); } private Flux<String> doRequest(String url) { return Flux.just("{\"quote\": \"inspiring quote from " + url + "\"}"); } private Mono<String> scatterAndGather(Flux<String> urls) { return urls.flatMap(url -> doRequest(url)) .single(); } @Rule public TestName testName = new TestName(); @Before public void populateDebug() { if (testName.getMethodName().equals("debuggingCommonStacktrace")) { toDebug = scatterAndGather(urls()); } else if (testName.getMethodName().equals("debuggingActivatedForSpecific")) { Hooks.onOperator(hook -> hook .ifNameContains("single") .operatorStacktrace()); toDebug = scatterAndGather(urls()); } else if (testName.getMethodName().startsWith("debuggingActivated")) { Hooks.onOperator(Hooks.OperatorHook::operatorStacktrace); toDebug = scatterAndGather(urls()); } } @After public void removeHooks() { if (testName.getMethodName().startsWith("debuggingActivated")) { Hooks.resetOnOperator(); } } public Mono<String> toDebug; //please overlook the public class attribute :p private void printAndAssert(Throwable t, boolean checkForAssemblySuppressed) { t.printStackTrace(); assertThat(t) .isInstanceOf(IndexOutOfBoundsException.class) .hasMessage("Source emitted more than one item"); if (!checkForAssemblySuppressed) { assertThat(t).hasNoSuppressedExceptions(); } else { assertThat(t).satisfies(withSuppressed -> { assertThat(withSuppressed.getSuppressed()).hasSize(1); assertThat(withSuppressed.getSuppressed()[0]) .hasMessageStartingWith("\nAssembly trace from producer [reactor.core.publisher.MonoSingle] :") .hasMessageEndingWith("Flux.single(GuideTests.java:824)\n"); }); } } @Test public void debuggingCommonStacktrace() { toDebug.subscribe(System.out::println, t -> printAndAssert(t, false)); } @Test public void debuggingActivated() { toDebug.subscribe(System.out::println, t -> printAndAssert(t, true)); } @Test public void debuggingActivatedForSpecific() { toDebug.subscribe(System.out::println, t -> printAndAssert(t, true)); } @Test public void debuggingLogging() { Flux<Integer> flux = Flux.range(1, 10) .log() .take(3); //flux.subscribe(); //nothing much to test, but... StepVerifier.create(flux).expectNext(1, 2, 3).verifyComplete(); } }