/** * Create a new WorkQueueProcessor using {@link Queues#SMALL_BUFFER_SIZE} backlog size, * blockingWait Strategy and auto-cancel. <p> A new Cached ThreadExecutorPool will be * implicitly created. * @param <E> Type of processed signals * @return a fresh processor */ public static <E> WorkQueueProcessor<E> create() { return WorkQueueProcessor.<E>builder().build(); }
@Test(timeout = 15000L) public void completeDoesNotHang() throws Exception { WorkQueueProcessor<String> wq = WorkQueueProcessor.create(); wq.subscribe(); Assert.assertTrue(wq.downstreamCount() == 1); wq.onComplete(); while (wq.downstreamCount() != 0 && Thread.activeCount() > 2) { } }
/** * Creates a new {@link WorkQueueProcessor} using the properties * of this builder. * @return a fresh processor */ public WorkQueueProcessor<T> build() { String name = this.name != null ? this.name : WorkQueueProcessor.class.getSimpleName(); WaitStrategy waitStrategy = this.waitStrategy != null ? this.waitStrategy : WaitStrategy.liteBlocking(); ThreadFactory threadFactory = this.executor != null ? null : new EventLoopFactory(name, autoCancel); ExecutorService requestTaskExecutor = this.requestTaskExecutor != null ? this.requestTaskExecutor : defaultRequestTaskExecutor(defaultName(threadFactory, WorkQueueProcessor.class)); return new WorkQueueProcessor<>( threadFactory, executor, requestTaskExecutor, bufferSize, waitStrategy, share, autoCancel); } }
@Test public void scanProcessor() { WorkQueueProcessor<String> test = WorkQueueProcessor.create("name", 16); Subscription subscription = Operators.emptySubscription(); test.onSubscribe(subscription); Assertions.assertThat(test.scan(Scannable.Attr.PARENT)).isEqualTo(subscription); Assertions.assertThat(test.scan(Scannable.Attr.CAPACITY)).isEqualTo(16); Assertions.assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); Assertions.assertThat(test.scan(Scannable.Attr.ERROR)).isNull(); test.onError(new IllegalStateException("boom")); Assertions.assertThat(test.scan(Scannable.Attr.ERROR)).hasMessage("boom"); Assertions.assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); }
@Test(timeout = 15000L) public void cancelDoesNotHang() throws Exception { WorkQueueProcessor<String> wq = WorkQueueProcessor.create(); Disposable d = wq.subscribe(); Assert.assertTrue(wq.downstreamCount() == 1); d.dispose(); while (wq.downstreamCount() != 0 && Thread.activeCount() > 2) { } }
@Test public void retryErrorPropagatedFromWorkQueueSubscriberCold() throws Exception { AtomicInteger errors = new AtomicInteger(3); WorkQueueProcessor<Integer> wq = WorkQueueProcessor.<Integer>builder().autoCancel(false).build(); AtomicInteger onNextSignals = new AtomicInteger(); wq.onNext(1); wq.onNext(2); wq.onNext(3); wq.onComplete(); StepVerifier.create(wq.log("wq", Level.FINE) .doOnNext(e -> onNextSignals.incrementAndGet()).<Integer>handle( (s1, sink) -> { if (errors.decrementAndGet() > 0) { sink.error(new RuntimeException()); } else { sink.next(s1); } }).retry()) .expectNext(1, 2, 3) .verifyComplete(); assertThat(onNextSignals.get(), equalTo(5)); while (wq.downstreamCount() != 0 && Thread.activeCount() > 1) { } }
throws Exception { AtomicInteger errors = new AtomicInteger(3); WorkQueueProcessor<Integer> wq = WorkQueueProcessor.<Integer>builder().autoCancel(false).build(); AtomicInteger onNextSignals = new AtomicInteger(); StepVerifier.create(wq.publishOn(Schedulers.parallel(), 1) .publish() .autoConnect() .retry()) .then(() -> { wq.onNext(1); wq.onNext(2); wq.onNext(3); }) .expectNext(3) wq.onComplete(); while (wq.downstreamCount() != 0 && Thread.activeCount() > 1) {
@Test public void fixedThreadPoolWorkQueueRejectsSubscribers() { ExecutorService executorService = Executors.newFixedThreadPool(2); WorkQueueProcessor<String> bc = WorkQueueProcessor.<String>builder().executor(executorService).bufferSize(16).build(); CountDownLatch latch = new CountDownLatch(3); TestWorkQueueSubscriber spec1 = new TestWorkQueueSubscriber(latch, "spec1"); TestWorkQueueSubscriber spec2 = new TestWorkQueueSubscriber(latch, "spec2"); TestWorkQueueSubscriber spec3 = new TestWorkQueueSubscriber(latch, "spec3"); bc.subscribe(spec1); bc.subscribe(spec2); bc.subscribe(spec3); bc.onNext("foo"); bc.onComplete(); assertThat(spec1.error, is(nullValue())); assertThat(spec2.error, is(nullValue())); assertThat(spec3.error, is(notNullValue())); assertThat(spec3.error.getMessage(), startsWith( "The executor service could not accommodate another subscriber, detected limit 2")); try { latch.await(1, TimeUnit.SECONDS); } catch (InterruptedException e1) { fail(e1.toString()); } }
@Test() public void retryNoThreadLeak() throws Exception { WorkQueueProcessor<Integer> wq = WorkQueueProcessor.<Integer>builder().autoCancel(false).build(); wq.handle((integer, sink) -> sink.error(new RuntimeException())) .retry(10) .subscribe(); wq.onNext(1); wq.onNext(2); wq.onNext(3); wq.onComplete(); while (wq.downstreamCount() != 0 && Thread.activeCount() > 1) { } }
@Test public void testForceShutdownAfterShutdown() throws InterruptedException { WorkQueueProcessor<String> processor = WorkQueueProcessor.<String>builder() .name("processor").bufferSize(4) .waitStrategy(WaitStrategy.phasedOffLiteLock(200, 100, TimeUnit.MILLISECONDS)) //eliminate the waitstrategy diff .build(); Publisher<String> publisher = Flux.fromArray(new String[] { "1", "2", "3", "4", "5" }); publisher.subscribe(processor); AssertSubscriber<String> subscriber = AssertSubscriber.create(0); processor.subscribe(subscriber); subscriber.request(1); Thread.sleep(250); processor.shutdown(); assertFalse(processor.awaitAndShutdown(Duration.ofMillis(400))); processor.forceShutdown(); assertTrue(processor.awaitAndShutdown(Duration.ofMillis(400))); }
@Test public void testCustomRequestTaskThreadNameShare() { String expectedName = "workQueueProcessorRequestTaskShare"; //NOTE: the below single executor should not be used usually as requestTask assumes it immediately gets executed ExecutorService customTaskExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, expectedName)); WorkQueueProcessor<Object> processor = WorkQueueProcessor.builder() .executor(Executors.newCachedThreadPool()) .requestTaskExecutor(customTaskExecutor) .bufferSize(8) .waitStrategy(WaitStrategy.liteBlocking()) .autoCancel(true) .build(); processor.requestTask(Operators.cancelledSubscription()); processor.subscribe(); Thread[] threads = new Thread[Thread.activeCount()]; Thread.enumerate(threads); //cleanup to avoid visibility in other tests customTaskExecutor.shutdownNow(); processor.forceShutdown(); Condition<Thread> customRequestTaskThread = new Condition<>( thread -> thread != null && expectedName.equals(thread.getName()), "a thread named \"%s\"", expectedName); Assertions.assertThat(threads) .haveExactly(1, customRequestTaskThread); }
@Test public void testWorkQueueProcessorGetters() { final int TEST_BUFFER_SIZE = 16; WorkQueueProcessor<Object> processor = WorkQueueProcessor.builder().name("testProcessor").bufferSize(TEST_BUFFER_SIZE).build(); assertEquals(TEST_BUFFER_SIZE, processor.getAvailableCapacity()); processor.onNext(new Object()); assertEquals(TEST_BUFFER_SIZE - 1, processor.getAvailableCapacity()); processor.awaitAndShutdown(); }
@Test public void nonSerializedSinkMultiProducer() throws Exception { int count = 1000; WorkQueueProcessor<Integer> queueProcessor = WorkQueueProcessor.<Integer>builder() .share(true) .build(); TestSubscriber subscriber = new TestSubscriber(count); queueProcessor.subscribe(subscriber); FluxSink<Integer> sink = queueProcessor.sink(); Assertions.assertThat(sink).isNotInstanceOf(SerializedSink.class); for (int i = 0; i < count; i++) { sink = sink.next(i); Assertions.assertThat(sink).isNotInstanceOf(SerializedSink.class); } subscriber.await(Duration.ofSeconds(5)); assertNull("Unexpected exception in subscriber", subscriber.failure); }
@Test(timeout = 4000) public void singleThreadWorkQueueSucceedsWithOneSubscriber() { ExecutorService executorService = Executors.newSingleThreadExecutor(); WorkQueueProcessor<String> bc = WorkQueueProcessor.<String>builder().executor(executorService).bufferSize(2).build(); CountDownLatch latch = new CountDownLatch(1); TestWorkQueueSubscriber spec1 = new TestWorkQueueSubscriber(latch, "spec1"); bc.subscribe(spec1); bc.onNext("foo"); bc.onNext("bar"); Executors.newSingleThreadScheduledExecutor() .schedule(bc::onComplete, 200, TimeUnit.MILLISECONDS); bc.onNext("baz"); try { latch.await(800, TimeUnit.MILLISECONDS); } catch (InterruptedException e1) { fail(e1.toString()); } assertNull(spec1.error); }
@Test public void retryErrorPropagatedFromWorkQueueSubscriberHot() throws Exception { AtomicInteger errors = new AtomicInteger(3); WorkQueueProcessor<Integer> wq = WorkQueueProcessor.<Integer>builder().autoCancel(false).build(); AtomicInteger onNextSignals = new AtomicInteger(); StepVerifier.create(wq.doOnNext(e -> onNextSignals.incrementAndGet()).<Integer>handle( (s1, sink) -> { if (errors.decrementAndGet() > 0) { sink.error(new RuntimeException("expected")); } else { sink.next(s1); } }).retry()) .then(() -> { wq.onNext(1); wq.onNext(2); wq.onNext(3); }) .expectNext(3) .thenCancel() .verify(); assertThat(onNextSignals.get(), equalTo(3)); while (wq.downstreamCount() != 0 && Thread.activeCount() > 1) { } }
public void retryErrorPropagatedFromWorkQueueSubscriberHotPoisonSignalFlatMapPrefetch1() throws Exception { WorkQueueProcessor<Integer> wq = WorkQueueProcessor.<Integer>builder().autoCancel(false).build(); AtomicInteger onNextSignals = new AtomicInteger(); StepVerifier.create(wq.flatMap(i -> Mono.just(i) .doOnNext(e -> onNextSignals.incrementAndGet()).<Integer>handle( (s1, sink) -> { .retry()) .then(() -> { wq.onNext(1); wq.onNext(2); wq.onNext(3); }) .expectNextMatches(d -> d == 2 || d == 3) .verify(); wq.onComplete(); while (wq.downstreamCount() != 0 && Thread.activeCount() > 1) {
@Test public void retryErrorPropagatedFromWorkQueueSubscriberHotPoisonSignalFlatMap() throws Exception { WorkQueueProcessor<Integer> wq = WorkQueueProcessor.<Integer>builder().autoCancel(false).build(); AtomicInteger onNextSignals = new AtomicInteger(); StepVerifier.create(wq.publish() .autoConnect() .flatMap(i -> Mono.just(i) .retry()) .then(() -> { wq.onNext(1); wq.onNext(2); wq.onNext(3); }) .expectNextMatches(d -> d == 2 || d == 3) .verify(); wq.onComplete(); while (wq.downstreamCount() != 0 && Thread.activeCount() > 1) {
@Test public void retryErrorPropagatedFromWorkQueueSubscriberHotPoisonSignalParallel() throws Exception { WorkQueueProcessor<Integer> wq = WorkQueueProcessor.<Integer>builder().autoCancel(false).build(); AtomicInteger onNextSignals = new AtomicInteger(); StepVerifier.create(wq.parallel(4) .runOn(Schedulers.newParallel("par", 4)) .transform(flux -> ParallelFlux.from(flux.groups() .retry()) .then(() -> { wq.onNext(1); wq.onNext(2); wq.onNext(3); }) .expectNextMatches(d -> d == 2 || d == 3) wq.onComplete(); while (wq.downstreamCount() != 0 && Thread.activeCount() > 1) {
@Override public Sink createSink(FlowConstruct flowConstruct, ReactiveProcessor function) { final long shutdownTimeout = flowConstruct.getMuleContext().getConfiguration().getShutdownTimeout(); WorkQueueProcessor<CoreEvent> processor = WorkQueueProcessor.<CoreEvent>builder().executor(ringBufferSchedulerSupplier.get()).bufferSize(bufferSize) .waitStrategy(waitStrategy.getReactorWaitStrategy()).build(); int subscriberCount = maxConcurrency < subscribers ? maxConcurrency : subscribers; CountDownLatch completionLatch = new CountDownLatch(subscriberCount); for (int i = 0; i < subscriberCount; i++) { processor.doOnSubscribe(subscription -> currentThread().setContextClassLoader(executionClassloader)).transform(function) .doFinally(s -> completionLatch.countDown()).subscribe(); } return new ReactorSink(processor.sink(), () -> { long start = currentTimeMillis(); if (!processor.awaitAndShutdown(ofMillis(shutdownTimeout))) { processor.forceShutdown(); } try { completionLatch.await(max(start - currentTimeMillis() + shutdownTimeout, 0l), MILLISECONDS); } catch (InterruptedException e) { currentThread().interrupt(); throw new MuleRuntimeException(e); } }, createOnEventConsumer(), bufferSize); }
@Test(timeout = 5_000) public void testBufferSize1Shared() throws Exception { WorkQueueProcessor<String> broadcast = WorkQueueProcessor.<String>builder() .share(true) .name("share-name") .bufferSize(1) .autoCancel(true) .build(); int simultaneousSubscribers = 3000; CountDownLatch latch = new CountDownLatch(simultaneousSubscribers); Scheduler scheduler = Schedulers.single(); FluxSink<String> sink = broadcast.sink(); Flux<String> flux = broadcast.filter(Objects::nonNull) .publishOn(scheduler) .cache(1); for (int i = 0; i < simultaneousSubscribers; i++) { flux.subscribe(s -> latch.countDown()); } sink.next("data"); Assertions.assertThat(latch.await(4, TimeUnit.SECONDS)) .overridingErrorMessage("Data not received") .isTrue(); }