/* * 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.core.publisher; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.LongAdder; import java.util.function.Supplier; import org.assertj.core.api.Assertions; import org.junit.Assert; import org.junit.Test; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import reactor.test.subscriber.AssertSubscriber; import reactor.util.concurrent.QueueSupplier; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertTrue; public class ParallelFluxTest { @Test public void sequentialMode() { Flux<Integer> source = Flux.range(1, 1_000_000) .hide(); for (int i = 1; i < 33; i++) { Flux<Integer> result = ParallelFlux.from(source, i) .map(v -> v + 1) .sequential(); AssertSubscriber<Integer> ts = AssertSubscriber.create(); result.subscribe(ts); ts.assertSubscribed() .assertValueCount(1_000_000) .assertComplete() .assertNoError(); } } @Test public void sequentialModeFused() { Flux<Integer> source = Flux.range(1, 1_000_000); for (int i = 1; i < 33; i++) { Flux<Integer> result = ParallelFlux.from(source, i) .map(v -> v + 1) .sequential(); AssertSubscriber<Integer> ts = AssertSubscriber.create(); result.subscribe(ts); ts.assertSubscribed() .assertValueCount(1_000_000) .assertComplete() .assertNoError(); } } @Test public void parallelMode() { Flux<Integer> source = Flux.range(1, 1_000_000) .hide(); int ncpu = Math.max(8, Runtime.getRuntime() .availableProcessors()); for (int i = 1; i < ncpu + 1; i++) { Scheduler scheduler = Schedulers.newParallel("test", i); try { Flux<Integer> result = ParallelFlux.from(source, i) .runOn(scheduler) .map(v -> v + 1) .sequential(); AssertSubscriber<Integer> ts = AssertSubscriber.create(); result.subscribe(ts); ts.await(Duration.ofSeconds(10)); ts.assertSubscribed() .assertValueCount(1_000_000) .assertComplete() .assertNoError(); } finally { scheduler.dispose(); } } } @Test public void parallelModeFused() { Flux<Integer> source = Flux.range(1, 1_000_000); int ncpu = Math.max(8, Runtime.getRuntime() .availableProcessors()); for (int i = 1; i < ncpu + 1; i++) { Scheduler scheduler = Schedulers.newParallel("test", i); try { Flux<Integer> result = ParallelFlux.from(source, i) .runOn(scheduler) .map(v -> v + 1) .sequential(); AssertSubscriber<Integer> ts = AssertSubscriber.create(); result.subscribe(ts); ts.await(Duration.ofSeconds(10)); ts.assertSubscribed() .assertValueCount(1_000_000) .assertComplete() .assertNoError(); } finally { scheduler.dispose(); } } } @Test public void collectSortedList() { AssertSubscriber<List<Integer>> ts = AssertSubscriber.create(); Flux.just(10, 9, 8, 7, 6, 5, 4, 3, 2, 1) .parallel() .collectSortedList(Comparator.naturalOrder()) .subscribe(ts); ts.assertValues(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); } @Test public void sorted() { AssertSubscriber<Integer> ts = AssertSubscriber.create(0); Flux.just(10, 9, 8, 7, 6, 5, 4, 3, 2, 1) .parallel() .sorted(Comparator.naturalOrder()) .subscribe(ts); ts.assertNoValues(); ts.request(2); ts.assertValues(1, 2); ts.request(5); ts.assertValues(1, 2, 3, 4, 5, 6, 7); ts.request(3); ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); } @Test public void groupMerge() { AssertSubscriber<Integer> ts = AssertSubscriber.create(); Flux.range(1, 10) .parallel() .groups() .flatMap(v -> v) .subscribe(ts); ts.assertContainValues(new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))) .assertNoError() .assertComplete(); } @Test public void from() { AssertSubscriber<Integer> ts = AssertSubscriber.create(); ParallelFlux.from(Flux.range(1, 5), Flux.range(6, 5)) .sequential() .subscribe(ts); ts.assertContainValues(new HashSet<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))) .assertNoError() .assertComplete(); } @Test public void concatMapUnordered() { AssertSubscriber<Integer> ts = AssertSubscriber.create(); Flux.range(1, 5) .parallel() .concatMap(v -> Flux.range(v * 10 + 1, 3)) .sequential() .subscribe(ts); ts.assertValues(11, 12, 13, 21, 22, 23, 31, 32, 33, 41, 42, 43, 51, 52, 53) .assertNoError() .assertComplete(); } @Test public void flatMapUnordered() { AssertSubscriber<Integer> ts = AssertSubscriber.create(); Flux.range(1, 5) .parallel() .flatMap(v -> Flux.range(v * 10 + 1, 3)) .sequential() .subscribe(ts); ts.assertValues(11, 12, 13, 21, 22, 23, 31, 32, 33, 41, 42, 43, 51, 52, 53) .assertNoError() .assertComplete(); } @Test public void testDoOnEachSignal() throws InterruptedException { List<Signal<Integer>> signals = Collections.synchronizedList(new ArrayList<>(4)); List<Integer> values = Collections.synchronizedList(new ArrayList<>(2)); ParallelFlux<Integer> flux = Flux.just(1, 2) .parallel(3) .doOnEach(signals::add) .doOnEach(s -> { if (s.isOnNext()) values.add(s.get()); }); //we use a lambda subscriber and latch to avoid using `sequential` CountDownLatch latch = new CountDownLatch(2); flux.subscribe(v -> { }, e -> latch.countDown(), latch::countDown); assertTrue(latch.await(2, TimeUnit.SECONDS)); assertThat(signals.size()).isEqualTo(5); assertThat(signals.get(0).get()) .as("first onNext signal isn't first value") .isEqualTo(1); assertThat(signals.get(1).get()) .as("second onNext signal isn't last value") .isEqualTo(2); assertTrue("onComplete for rail 1 expected", signals.get(2) .isOnComplete()); assertTrue("onComplete for rail 2 expected", signals.get(3) .isOnComplete()); assertTrue("onComplete for rail 3 expected", signals.get(4) .isOnComplete()); assertThat(values.get(0)).as("1st onNext value unexpected").isEqualTo(1); assertThat(values.get(1)).as("2nd onNext value unexpected").isEqualTo(2); } @Test public void testDoOnEachSignalWithError() throws InterruptedException { List<Signal<Integer>> signals = Collections.synchronizedList(new ArrayList<>(4)); ParallelFlux<Integer> flux = Flux.<Integer>error(new IllegalArgumentException("boom")).parallel(2) .runOn(Schedulers.parallel()) .doOnEach(signals::add); //we use a lambda subscriber and latch to avoid using `sequential` CountDownLatch latch = new CountDownLatch(2); flux.subscribe(v -> { }, e -> latch.countDown(), latch::countDown); assertTrue(latch.await(2, TimeUnit.SECONDS)); assertThat(signals).hasSize(2); assertTrue("rail 1 onError expected", signals.get(0) .isOnError()); assertTrue("rail 2 onError expected", signals.get(1) .isOnError()); assertThat(signals.get(0).getThrowable()).as("plain exception rail 1 expected") .hasMessage("boom"); assertThat(signals.get(1).getThrowable()).as("plain exception rail 2 expected") .hasMessage("boom"); } @Test(expected = NullPointerException.class) public void testDoOnEachSignalNullConsumer() { Flux.just(1) .parallel() .doOnEach(null); } @Test public void testDoOnEachSignalToSubscriber() { AssertSubscriber<Integer> peekSubscriber = AssertSubscriber.create(); ParallelFlux<Integer> flux = Flux.just(1, 2) .parallel(3) .doOnEach(s -> s.accept(peekSubscriber)); flux.subscribe(); peekSubscriber.assertNotSubscribed(); peekSubscriber.assertValues(1, 2); Assertions.assertThatExceptionOfType(AssertionError.class) .isThrownBy(peekSubscriber::assertComplete) .withMessage("Multiple completions: 3"); } @Test public void composeGroup() { Set<Integer> values = new ConcurrentSkipListSet<>(); Flux<Integer> flux = Flux.range(1, 10) .parallel(3) .runOn(Schedulers.parallel()) .doOnNext(values::add) .composeGroup(p -> p.log("rail" + p.key()) .map(i -> (p.key() + 1) * 100 + i)) .sequential(); StepVerifier.create(flux.sort()) .assertNext(i -> assertThat(i - 100) .isBetween(1, 10)) .thenConsumeWhile(i -> i / 100 == 1) .assertNext(i -> assertThat(i - 200) .isBetween(1, 10)) .thenConsumeWhile(i -> i / 100 == 2) .assertNext(i -> assertThat(i - 300) .isBetween(1, 10)) .thenConsumeWhile(i -> i / 100 == 3) .verifyComplete(); assertThat(values) .hasSize(10) .contains(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); } @Test public void fromSourceHasCpuParallelism() { int cpus = Runtime.getRuntime() .availableProcessors(); ParallelFlux<Integer> parallelFlux = ParallelFlux.from(Flux.range(1, 10)); assertThat(parallelFlux.parallelism()) .isEqualTo(cpus); } @Test public void fromZeroParallelismRejected() { Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> ParallelFlux.from(Mono.just(1), 0)) .withMessage("parallelism > 0 required but it was 0"); } @Test public void fromNegativeParallelismRejected() { Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> ParallelFlux.from(Mono.just(1), -1)) .withMessage("parallelism > 0 required but it was -1"); } @Test public void fromZeroPrefetchRejected() { Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> ParallelFlux.from(Mono.just(1), 1, 0, QueueSupplier.small())) .withMessage("prefetch > 0 required but it was 0"); } @Test public void fromNegativePrefetchRejected() { Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> ParallelFlux.from(Mono.just(1), 1, -1, QueueSupplier.small())) .withMessage("prefetch > 0 required but it was -1"); } @Test public void fromZeroPublishersRejected() { Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> ParallelFlux.<Integer>from()) .withMessage("Zero publishers not supported"); } @Test @SuppressWarnings("unchecked") public void fromZeroLengthArrayPublishersRejected() { Publisher<Integer>[] array = new Publisher[0]; Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> ParallelFlux.from(array)) .withMessage("Zero publishers not supported"); } @Test public void fromNullPublisherRejected() { Assertions.assertThatExceptionOfType(NullPointerException.class) .isThrownBy(() -> ParallelFlux.from((Publisher<?>) null)) .withMessage("source"); } @Test @SuppressWarnings("unchecked") public void fromNullPublisherArrayRejected() { Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> ParallelFlux.from((Publisher[]) null)) .withMessage("Zero publishers not supported"); } @Test public void runOnZeroPrefetchRejected() { ParallelFlux<Integer> validSoFar = ParallelFlux.from(Mono.just(1)); Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> validSoFar.runOn(Schedulers.parallel(), 0)) .withMessage("prefetch > 0 required but it was 0"); } @Test public void runOnNegativePrefetchRejected() { ParallelFlux<Integer> validSoFar = ParallelFlux.from(Mono.just(1)); Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> validSoFar.runOn(Schedulers.parallel(), -1)) .withMessage("prefetch > 0 required but it was -1"); } @Test public void sequentialZeroPrefetchRejected() { ParallelFlux<Integer> validSoFar = ParallelFlux.from(Mono.just(1)); Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> validSoFar.sequential(0)) .withMessage("prefetch > 0 required but it was 0"); } @Test public void sequentialNegativePrefetchRejected() { ParallelFlux<Integer> validSoFar = ParallelFlux.from(Mono.just(1)); Assertions.assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> validSoFar.sequential(-1)) .withMessage("prefetch > 0 required but it was -1"); } @Test public void subscribeOnNextOnErrorErrorsOnAllRails() { LongAdder valueAdder = new LongAdder(); LongAdder errorAdder = new LongAdder(); Flux.range(1, 3) .concatWith(Mono.error(new IllegalStateException("boom"))) .parallel(2) .subscribe(v -> valueAdder.increment(), e -> errorAdder.increment()); assertThat(valueAdder.intValue()).isEqualTo(3); assertThat(errorAdder.intValue()).isEqualTo(2); } @Test public void validateTooFewSubscribers() { validateSubscribers(2); } @Test public void validateTooManySubscribers() { validateSubscribers(4); } @SuppressWarnings("unchecked") private void validateSubscribers(int size) { List<Throwable> errors = Collections.synchronizedList(new ArrayList<>(size)); Subscriber<Integer>[] subs = new Subscriber[size]; for (int i = 0; i < subs.length; i++) { subs[i] = new BaseSubscriber<Integer>() { @Override protected void hookOnSubscribe(Subscription subscription) { requestUnbounded(); } @Override protected void hookOnNext(Integer value) { } @Override protected void hookOnError(Throwable throwable) { errors.add(throwable); } }; } Flux.range(1, 3) .parallel(3) .validate(subs); assertThat(errors) .hasSize(size) .allSatisfy(e -> assertThat(e).hasMessage("parallelism = 3, subscribers = " + size)); } @Test public void fromPublishersDefaultPrefetchIsMinusOne() { assertThat(ParallelFlux.from(Flux.range(1, 5), Flux.range(5, 5)) .getPrefetch()).isEqualTo(-1); } @Test public void fromPublisherDefaultPrefetchIsSmallBufferSize() { assertThat(ParallelFlux.from(Flux.range(1, 5)) .getPrefetch()).isEqualTo(QueueSupplier.SMALL_BUFFER_SIZE); } @Test public void fromPublishersSequentialSubscribe() { List<Integer> values = Collections.synchronizedList(new ArrayList<>(10)); ParallelFlux.from(Flux.range(1, 3), Flux.range(4, 3)) .runOn(Schedulers.parallel()) .doOnNext(values::add) .sequential() .blockLast(); assertThat(values) .hasSize(6) .containsExactlyInAnyOrder(1, 2, 3, 4, 5, 6); } @Test public void asChangesParallelism() { assertThat(ParallelFlux.from(Flux.range(1, 10), 3) .as(pf -> ParallelFlux.from(pf.sequential(), 5) .log("secondParallel")) .parallelism()) .isEqualTo(5); } @Test public void transformChangesPrefetch() { assertThat(ParallelFlux.from(Flux.range(1, 10), 3, 12, QueueSupplier.small()) .transform(pf -> pf.runOn(Schedulers.parallel(), 3) .log() .hide()) .getPrefetch()) .isEqualTo(3); } @Test public void testPeekComplete() { List<Signal> signals = Collections.synchronizedList(new ArrayList<>()); LongAdder subscribeCount = new LongAdder(); LongAdder valueCount = new LongAdder(); LongAdder requestCount = new LongAdder(); LongAdder completeCount = new LongAdder(); LongAdder cancelCount = new LongAdder(); LongAdder errorCount = new LongAdder(); LongAdder terminateCount = new LongAdder(); LongAdder afterTerminateCount = new LongAdder(); ParallelFlux.from(Flux.range(1, 10), 2) .doOnEach(signals::add) .doOnSubscribe(s -> subscribeCount.increment()) .doOnNext(v -> valueCount.increment()) .doOnRequest(r -> requestCount.increment()) .doOnComplete(completeCount::increment) .doOnCancel(cancelCount::increment) .doOnError(e -> errorCount.increment()) .doOnTerminate(terminateCount::increment) .doAfterTerminate(afterTerminateCount::increment) .subscribe(v -> {}); assertThat(signals).as("signals").hasSize(10 + 2); //2x5 onNext, 2x1 onComplete assertThat(subscribeCount.longValue()).as("subscribe").isEqualTo(2); //1 per rail assertThat(valueCount.longValue()).as("values").isEqualTo(10); assertThat(requestCount.longValue()).as("request").isEqualTo(2); //1 per rail assertThat(completeCount.longValue()).as("complete").isEqualTo(2); //1 per rail assertThat(cancelCount.longValue()).as("cancel").isEqualTo(0); assertThat(errorCount.longValue()).as("errors").isEqualTo(0); assertThat(terminateCount.longValue()).as("terminate").isEqualTo(2); //1 per rail assertThat(afterTerminateCount.longValue()).as("afterTerminate").isEqualTo(2); //1 per rail } @Test public void testPeekError() { List<Signal> signals = Collections.synchronizedList(new ArrayList<>()); LongAdder subscribeCount = new LongAdder(); LongAdder valueCount = new LongAdder(); LongAdder requestCount = new LongAdder(); LongAdder completeCount = new LongAdder(); LongAdder cancelCount = new LongAdder(); LongAdder errorCount = new LongAdder(); LongAdder terminateCount = new LongAdder(); LongAdder afterTerminateCount = new LongAdder(); ParallelFlux.from(Flux.range(1, 4).concatWith(Mono.error(new IllegalStateException("boom"))), 2) .doOnEach(signals::add) .doOnSubscribe(s -> subscribeCount.increment()) .doOnNext(v -> valueCount.increment()) .doOnRequest(r -> requestCount.increment()) .doOnComplete(completeCount::increment) .doOnCancel(cancelCount::increment) .doOnError(e -> errorCount.increment()) .doOnTerminate(terminateCount::increment) .doAfterTerminate(afterTerminateCount::increment) .subscribe(v -> {}, e -> {}); //error callback so that afterTerminate isn't swallowed assertThat(signals).as("signals").hasSize(4 + 2); //2x2 onNext, 2x1 onError assertThat(subscribeCount.longValue()).as("subscribe").isEqualTo(2); //1 per rail assertThat(valueCount.longValue()).as("values").isEqualTo(4); assertThat(requestCount.longValue()).as("request").isEqualTo(2); //1 per rail assertThat(completeCount.longValue()).as("complete").isEqualTo(0); assertThat(cancelCount.longValue()).as("cancel").isEqualTo(0); assertThat(errorCount.longValue()).as("errors").isEqualTo(2); assertThat(terminateCount.longValue()).as("terminate").isEqualTo(2); //1 per rail assertThat(afterTerminateCount.longValue()).as("afterTerminate").isEqualTo(2); //1 per rail } @Test public void testPeekCancel() { List<Signal> signals = Collections.synchronizedList(new ArrayList<>()); LongAdder subscribeCount = new LongAdder(); LongAdder valueCount = new LongAdder(); LongAdder requestCount = new LongAdder(); LongAdder completeCount = new LongAdder(); LongAdder cancelCount = new LongAdder(); LongAdder errorCount = new LongAdder(); LongAdder terminateCount = new LongAdder(); LongAdder afterTerminateCount = new LongAdder(); ParallelFlux.from(Flux.range(1, 10), 2) .doOnEach(signals::add) .doOnSubscribe(s -> subscribeCount.increment()) .doOnNext(v -> valueCount.increment()) .doOnRequest(r -> requestCount.increment()) .doOnComplete(completeCount::increment) .doOnCancel(cancelCount::increment) .doOnError(e -> errorCount.increment()) .doOnTerminate(terminateCount::increment) .doAfterTerminate(afterTerminateCount::increment) .sequential().take(4).subscribe(); assertThat(signals).as("signals").hasSize(4); //2x2 onNext (+ 2 non-represented cancels) assertThat(subscribeCount.longValue()).as("subscribe").isEqualTo(2); //1 per rail assertThat(valueCount.longValue()).as("values").isEqualTo(4); assertThat(requestCount.longValue()).as("request").isEqualTo(2); //1 per rail assertThat(completeCount.longValue()).as("complete").isEqualTo(0); assertThat(cancelCount.longValue()).as("cancel").isEqualTo(2); assertThat(errorCount.longValue()).as("errors").isEqualTo(0); //cancel don't trigger onTerminate/onAfterTerminate: assertThat(terminateCount.longValue()).as("terminate").isEqualTo(0); assertThat(afterTerminateCount.longValue()).as("afterTerminate").isEqualTo(0); } @Test public void testConcatMapPrefetch() { ParallelFlux<Integer> pf = ParallelFlux.from(Flux.range(1, 4), 2) .concatMap(i -> Flux.just(i, 100 * i), 4); assertThat(pf.getPrefetch()).isEqualTo(4); StepVerifier.create(pf) .expectNext(1, 100, 2, 200, 3, 300, 4, 400) .verifyComplete(); } @Test public void testConcatMapDelayError() { ParallelFlux<Integer> pf = ParallelFlux.from(Flux.range(1, 4), 2) .concatMapDelayError(i -> { if (i == 1) return Mono.error(new IllegalStateException("boom")); return Flux.just(i, 100 * i); }); StepVerifier.create(pf) .expectNext(2, 200, 3, 300, 4, 400) .verifyErrorMessage("boom"); } @Test public void testConcatMapDelayErrorPrefetch() { ParallelFlux<Integer> pf = ParallelFlux.from(Flux.range(1, 4), 2) .concatMapDelayError(i -> { if (i == 1) return Mono.error(new IllegalStateException("boom")); return Flux.just(i, 100 * i); }, 4); assertThat(pf.getPrefetch()).isEqualTo(4); StepVerifier.create(pf) .expectNext(2, 200, 3, 300, 4, 400) .verifyErrorMessage("boom"); } @Test public void testConcatMapDelayErrorPrefetchDelayUntilEnd() { ParallelFlux<Integer> pf = ParallelFlux.from(Flux.range(1, 4), 2) .concatMapDelayError(i -> { if (i == 1) return Mono.error(new IllegalStateException("boom")); return Flux.just(i, 100 * i); }, false, 4); assertThat(pf.getPrefetch()).isEqualTo(4); StepVerifier.create(pf) .verifyErrorMessage("boom"); } @Test public void testFlatMapDelayError() { ParallelFlux<Integer> pf = ParallelFlux.from(Flux.range(1, 4), 2) .flatMap(i -> { if (i == 1) return Mono.error(new IllegalStateException("boom")); return Flux.just(i, 100 * i); }, true); StepVerifier.create(pf) .expectNext(2, 200, 3, 300, 4, 400) .verifyErrorMessage("boom"); } @Test public void testFlatMapDelayErrorMaxConcurrency() { ParallelFlux<Integer> pf = ParallelFlux.from(Flux.range(1, 4), 2) .flatMap(i -> { if (i == 1) return Mono.error(new IllegalStateException("boom")); return Flux.just(i, 100 * i); }, true, 2); StepVerifier.create(pf) .expectNext(2, 200, 3, 300, 4, 400) .verifyErrorMessage("boom"); } @Test public void testPublisherSubscribeUsesSequential() { LongAdder valueCount = new LongAdder(); ParallelFlux<Integer> pf = ParallelFlux.from(Flux.range(1, 4), 2); pf.subscribe(new BaseSubscriber<Integer>() { @Override protected void hookOnSubscribe(Subscription subscription) { requestUnbounded(); } @Override protected void hookOnNext(Integer value) { valueCount.increment(); } }); assertThat(valueCount.intValue()).isEqualTo(4); } @Test public void collectSortedListBothEmpty() { List<Integer> result = ParallelFlux.sortedMerger(Collections.emptyList(), Collections.emptyList(), Integer::compareTo); assertThat(result) .isEmpty(); } @Test public void collectSortedListRightLarger() { List<Integer> left = Arrays.asList(1, 3); List<Integer> right = Arrays.asList(2, 4, 5, 6); List<Integer> result = ParallelFlux.sortedMerger(left, right, Integer::compareTo); assertThat(result) .containsExactly(1, 2, 3, 4, 5, 6); } @Test public void collectSortedListLeftLarger() { List<Integer> left = Arrays.asList(2, 4, 5, 6); List<Integer> right = Arrays.asList(1, 3); List<Integer> result = ParallelFlux.sortedMerger(left, right, Integer::compareTo); assertThat(result) .containsExactly(1, 2, 3, 4, 5, 6); } @Test public void collectSortedListLeftEmpty() { List<Integer> left = Collections.emptyList(); List<Integer> right = Arrays.asList(2, 4, 5, 6); List<Integer> result = ParallelFlux.sortedMerger(left, right, Integer::compareTo); assertThat(result) .containsExactly(2, 4, 5, 6); } @Test public void collectSortedListRightEmpty() { List<Integer> left = Arrays.asList(2, 4, 5, 6); List<Integer> right = Collections.emptyList(); List<Integer> result = ParallelFlux.sortedMerger(left, right, Integer::compareTo); assertThat(result) .containsExactly(2, 4, 5, 6); } @Test public void testParallelism() throws Exception { Flux<Integer> flux = Flux.just(1, 2, 3); Set<String> threadNames = Collections.synchronizedSet(new TreeSet<>()); AtomicInteger count = new AtomicInteger(); CountDownLatch latch = new CountDownLatch(3); flux // Uncomment line below for failure .cache(1) .parallel(3) .runOn(Schedulers.newElastic("TEST")) .subscribe(i -> { threadNames.add(Thread.currentThread() .getName()); count.incrementAndGet(); latch.countDown(); tryToSleep(1000); }); latch.await(); Assert.assertEquals("Multithreaded count", 3, count.get()); Assert.assertEquals("Multithreaded threads", 3, threadNames.size()); } private void tryToSleep(long value) { try { Thread.sleep(value); } catch(InterruptedException e) { e.printStackTrace(); } } }