/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.runtime.core.processor.strategy;
import static java.lang.Thread.currentThread;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.Collections.synchronizedSet;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mule.runtime.core.api.construct.Flow.builder;
import static org.mule.runtime.core.api.processor.ReactiveProcessor.ProcessingType.BLOCKING;
import static org.mule.runtime.core.api.processor.ReactiveProcessor.ProcessingType.CPU_LITE;
import static org.mule.runtime.core.api.processor.ReactiveProcessor.ProcessingType.CPU_LITE_ASYNC;
import static org.mule.runtime.core.exception.Errors.Identifiers.OVERLOAD_ERROR_IDENTIFIER;
import static org.mule.runtime.core.internal.util.rx.Operators.requestUnbounded;
import static reactor.core.Exceptions.bubble;
import static reactor.core.publisher.Flux.from;
import static reactor.core.publisher.Mono.just;
import static reactor.core.scheduler.Schedulers.fromExecutorService;
import org.mule.runtime.api.exception.MuleException;
import org.mule.runtime.api.scheduler.Scheduler;
import org.mule.runtime.core.api.Event;
import org.mule.runtime.core.api.MuleContext;
import org.mule.runtime.core.api.construct.Flow;
import org.mule.runtime.core.api.context.MuleContextBuilder;
import org.mule.runtime.core.api.context.notification.ServerNotification;
import org.mule.runtime.core.api.processor.Processor;
import org.mule.runtime.core.api.processor.ReactiveProcessor.ProcessingType;
import org.mule.runtime.core.api.processor.strategy.ProcessingStrategy;
import org.mule.runtime.core.api.registry.RegistrationException;
import org.mule.runtime.core.api.scheduler.SchedulerService;
import org.mule.runtime.core.context.notification.ServerNotificationManager;
import org.mule.runtime.core.exception.MessagingException;
import org.mule.runtime.core.util.concurrent.Latch;
import org.mule.runtime.core.util.concurrent.NamedThreadFactory;
import org.mule.tck.junit4.AbstractReactiveProcessorTestCase;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.reactivestreams.Publisher;
public abstract class AbstractProcessingStrategyTestCase extends AbstractReactiveProcessorTestCase {
protected static final String CPU_LIGHT = "cpuLight";
protected static final String IO = "I/O";
protected static final String CPU_INTENSIVE = "cpuIntensive";
protected static final String CUSTOM = "custom";
protected static final String RING_BUFFER = "ringBuffer";
private static final int STREAM_ITERATIONS = 2000;
private static final int CONCURRENT_TEST_CONCURRENCY = 8;
protected Flow flow;
protected Set<String> threads = synchronizedSet(new HashSet<>());
protected Processor cpuLightProcessor = new ThreadTrackingProcessor() {
@Override
public ProcessingType getProcessingType() {
return CPU_LITE;
}
};
protected Processor cpuIntensiveProcessor = new ThreadTrackingProcessor() {
@Override
public ProcessingType getProcessingType() {
return ProcessingType.CPU_INTENSIVE;
}
};
protected Processor blockingProcessor = new ThreadTrackingProcessor() {
@Override
public ProcessingType getProcessingType() {
return BLOCKING;
}
};
protected Processor asyncProcessor = new ThreadTrackingProcessor() {
@Override
public ProcessingType getProcessingType() {
return CPU_LITE_ASYNC;
}
};
protected Processor failingProcessor = new ThreadTrackingProcessor() {
@Override
public Event process(Event event) {
throw new RuntimeException("FAILURE");
}
};
protected Processor errorSuccessProcessor = new ThreadTrackingProcessor() {
private AtomicInteger count = new AtomicInteger();
@Override
public Event process(Event event) throws MuleException {
if (count.getAndIncrement() % 10 < 5) {
return super.process(event);
} else {
return failingProcessor.process(event);
}
}
};
protected Scheduler cpuLight;
protected Scheduler blocking;
protected Scheduler cpuIntensive;
protected Scheduler custom;
protected Scheduler ringBuffer;
protected Scheduler asyncExecutor;
@Rule
public ExpectedException expectedException = ExpectedException.none();
public AbstractProcessingStrategyTestCase(Mode mode) {
super(mode);
}
@Before
public void before() throws RegistrationException {
cpuLight = new TestScheduler(2, CPU_LIGHT);
blocking = new TestScheduler(4, IO);
cpuIntensive = new TestScheduler(2, CPU_INTENSIVE);
custom = new TestScheduler(1, CUSTOM);
ringBuffer = new TestScheduler(1, RING_BUFFER);
asyncExecutor = muleContext.getRegistry().lookupObject(SchedulerService.class).ioScheduler();
flow = builder("test", muleContext)
.processingStrategyFactory((muleContext, prefix) -> createProcessingStrategy(muleContext, prefix))
// Avoid logging of errors by using a null exception handler.
.messagingExceptionHandler((exception, event) -> event)
.build();
}
@Override
protected void configureMuleContext(MuleContextBuilder contextBuilder) {
super.configureMuleContext(contextBuilder);
contextBuilder.setNotificationManager(new ServerNotificationManager() {
@Override
public void fireNotification(ServerNotification notification) {
// Avoid processing of message processor notifications and verbose logging this may produce.
}
});
}
protected abstract ProcessingStrategy createProcessingStrategy(MuleContext muleContext, String schedulersNamePrefix);
@After
public void after() {
flow.dispose();
cpuLight.stop();
blocking.stop();
cpuIntensive.stop();
custom.stop();
asyncExecutor.stop();
}
@Test
public void singleCpuLight() throws Exception {
flow.setMessageProcessors(singletonList(cpuLightProcessor));
flow.initialise();
flow.start();
process(flow, testEvent());
}
@Test
public void singleCpuLightConcurrent() throws Exception {
internalConcurrent(false, CPU_LITE, 1);
}
@Test
public void singleBlockingConcurrent() throws Exception {
internalConcurrent(false, BLOCKING, 1);
}
protected void internalConcurrent(boolean blocks, ProcessingType processingType, int invocations,
Processor... processorsBeforeLatch)
throws MuleException, InterruptedException {
MultipleInvocationLatchedProcessor latchedProcessor = new MultipleInvocationLatchedProcessor(processingType, invocations);
List<Processor> processors = new ArrayList<>(asList(processorsBeforeLatch));
processors.add(latchedProcessor);
flow.setMessageProcessors(processors);
flow.initialise();
flow.start();
for (int i = 0; i < invocations; i++) {
asyncExecutor.submit(() -> process(flow, newEvent()));
}
latchedProcessor.getAllLatchedLatch().await();
asyncExecutor.submit(() -> process(flow, newEvent()));
assertThat(latchedProcessor.getUnlatchedInvocationLatch().await(BLOCK_TIMEOUT, MILLISECONDS), is(!blocks));
// We need to assert the threads logged at this point. But good idea to ensure once unlocked the pending invocation completes.
// To do this need to copy threads locally.
Set<String> threadsBeforeUnlock = new HashSet<>(threads);
latchedProcessor.release();
if (blocks) {
assertThat(latchedProcessor.getUnlatchedInvocationLatch().await(RECEIVE_TIMEOUT, MILLISECONDS), is(true));
}
threads = threadsBeforeUnlock;
}
@Test
public void multipleCpuLight() throws Exception {
flow.setMessageProcessors(asList(cpuLightProcessor, cpuLightProcessor, cpuLightProcessor));
flow.initialise();
flow.start();
process(flow, testEvent());
}
@Test
public void singleBlocking() throws Exception {
flow.setMessageProcessors(singletonList(blockingProcessor));
flow.initialise();
flow.start();
process(flow, testEvent());
}
@Test
public void multipleBlocking() throws Exception {
flow.setMessageProcessors(asList(blockingProcessor, blockingProcessor, blockingProcessor));
flow.initialise();
flow.start();
process(flow, testEvent());
}
@Test
public void singleCpuIntensive() throws Exception {
flow.setMessageProcessors(singletonList(cpuIntensiveProcessor));
flow.initialise();
flow.start();
process(flow, testEvent());
}
@Test
public void multipleCpuIntensive() throws Exception {
flow.setMessageProcessors(asList(cpuIntensiveProcessor, cpuIntensiveProcessor, cpuIntensiveProcessor));
flow.initialise();
flow.start();
process(flow, testEvent());
}
@Test
public void mix() throws Exception {
flow.setMessageProcessors(asList(cpuLightProcessor, cpuIntensiveProcessor, blockingProcessor));
flow.initialise();
flow.start();
process(flow, testEvent());
}
@Test
public void mix2() throws Exception {
flow.setMessageProcessors(asList(cpuLightProcessor, cpuLightProcessor, blockingProcessor, blockingProcessor,
cpuLightProcessor, cpuIntensiveProcessor, cpuIntensiveProcessor, cpuLightProcessor));
flow.initialise();
flow.start();
process(flow, testEvent());
}
@Test
public void asyncCpuLight() throws Exception {
flow.setMessageProcessors(asList(asyncProcessor, cpuLightProcessor));
flow.initialise();
flow.start();
process(flow, testEvent());
}
@Test
public void asyncCpuLightConcurrent() throws Exception {
internalConcurrent(false, CPU_LITE, 1, asyncProcessor);
}
@Test
public void stream() throws Exception {
flow.setMessageProcessors(asList(cpuLightProcessor));
flow.initialise();
flow.start();
CountDownLatch latch = new CountDownLatch(STREAM_ITERATIONS);
for (int i = 0; i < STREAM_ITERATIONS; i++) {
switch (mode) {
case BLOCKING:
flow.process(newEvent());
latch.countDown();
break;
case NON_BLOCKING:
processNonBlocking(flow, newEvent(), t -> latch.countDown(),
response -> bubble(new AssertionError("Unexpected error")));
}
}
assertThat(latch.await(RECEIVE_TIMEOUT, MILLISECONDS), is(true));
}
@Test
public void concurrentStream() throws Exception {
flow.setMessageProcessors(asList(cpuLightProcessor));
flow.initialise();
flow.start();
CountDownLatch latch = new CountDownLatch(STREAM_ITERATIONS);
for (int i = 0; i < CONCURRENT_TEST_CONCURRENCY; i++) {
asyncExecutor.submit(() -> {
for (int j = 0; j < STREAM_ITERATIONS / CONCURRENT_TEST_CONCURRENCY; j++) {
try {
switch (mode) {
case BLOCKING:
flow.process(newEvent());
latch.countDown();
break;
case NON_BLOCKING:
processNonBlocking(flow, newEvent(), t -> latch.countDown(),
response -> bubble(new AssertionError("Unexpected error")));
}
} catch (MuleException e) {
throw new RuntimeException(e);
}
}
});
}
assertThat(latch.await(RECEIVE_TIMEOUT, MILLISECONDS), is(true));
}
@Test
public void errorsStream() throws Exception {
flow.setMessageProcessors(asList(failingProcessor));
flow.initialise();
flow.start();
CountDownLatch latch = new CountDownLatch(STREAM_ITERATIONS);
for (int i = 0; i < STREAM_ITERATIONS; i++) {
switch (mode) {
case BLOCKING:
try {
flow.process(newEvent());
fail("Unexpected success");
} catch (Throwable t) {
latch.countDown();
}
break;
case NON_BLOCKING:
processNonBlocking(flow, newEvent(), response -> bubble(new AssertionError("Unexpected success")),
t -> latch.countDown());
}
}
assertThat(latch.await(RECEIVE_TIMEOUT, MILLISECONDS), is(true));
}
@Test
public void errorSuccessStream() throws Exception {
flow.setMessageProcessors(asList(errorSuccessProcessor));
flow.initialise();
flow.start();
CountDownLatch sucessLatch = new CountDownLatch(STREAM_ITERATIONS / 2);
CountDownLatch errorLatch = new CountDownLatch(STREAM_ITERATIONS / 2);
for (int i = 0; i < STREAM_ITERATIONS; i++) {
switch (mode) {
case BLOCKING:
try {
flow.process(newEvent());
sucessLatch.countDown();
} catch (Throwable t) {
errorLatch.countDown();
}
break;
case NON_BLOCKING:
processNonBlocking(flow, newEvent(), response -> sucessLatch.countDown(), t -> errorLatch.countDown());
}
}
assertThat(sucessLatch.await(RECEIVE_TIMEOUT, MILLISECONDS), is(true));
assertThat(errorLatch.await(RECEIVE_TIMEOUT, MILLISECONDS), is(true));
}
protected void processNonBlocking(Flow flow, Event event, Consumer<Event> onResponse, Consumer<Throwable> onError) {
just(event).transform(flow).subscribe(requestUnbounded());
from(event.getContext().getResponsePublisher()).subscribe(onResponse, onError);
}
@Test
public abstract void tx() throws Exception;
class MultipleInvocationLatchedProcessor implements Processor {
private ProcessingType type;
private volatile Latch latch = new Latch();
private volatile CountDownLatch allLatchedLatch;
private volatile Latch unlatchedInvocationLatch;
private AtomicInteger invocations;
public MultipleInvocationLatchedProcessor(ProcessingType type, int latchedInvocations) {
this.type = type;
allLatchedLatch = new CountDownLatch(latchedInvocations);
unlatchedInvocationLatch = new Latch();
invocations = new AtomicInteger(latchedInvocations);
}
@Override
public Event process(Event event) throws MuleException {
threads.add(currentThread().getName());
if (invocations.getAndDecrement() > 0) {
allLatchedLatch.countDown();
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
unlatchedInvocationLatch.countDown();
}
return event;
}
@Override
public ProcessingType getProcessingType() {
return type;
}
public void release() {
latch.release();
}
public CountDownLatch getAllLatchedLatch() throws InterruptedException {
return allLatchedLatch;
}
public Latch getUnlatchedInvocationLatch() throws InterruptedException {
return unlatchedInvocationLatch;
}
}
static class TestScheduler extends ScheduledThreadPoolExecutor implements Scheduler {
private ExecutorService executor;
public TestScheduler(int threads, String threadNamePrefix) {
super(1, new NamedThreadFactory(threadNamePrefix + ".tasks"));
executor = new ThreadPoolExecutor(threads, threads, 0l, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),
new NamedThreadFactory(threadNamePrefix));
}
@Override
public Future<?> submit(Runnable task) {
return executor.submit(task);
}
@Override
public void stop() {
shutdownNow();
executor.shutdownNow();
}
@Override
public ScheduledFuture<?> scheduleWithCronExpression(Runnable command, String cronExpression) {
throw new UnsupportedOperationException(
"Cron expression scheduling is not supported in unit tests. You need the productive service implementation.");
}
@Override
public ScheduledFuture<?> scheduleWithCronExpression(Runnable command, String cronExpression, TimeZone timeZone) {
throw new UnsupportedOperationException(
"Cron expression scheduling is not supported in unit tests. You need the productive service implementation.");
}
@Override
public String getName() {
return TestScheduler.class.getSimpleName();
}
}
static class RejectingScheduler extends TestScheduler {
public RejectingScheduler() {
super(1, "prefix");
}
@Override
public Future<?> submit(Runnable task) {
throw new RejectedExecutionException();
}
}
class ThreadTrackingProcessor implements Processor {
@Override
public Event process(Event event) throws MuleException {
threads.add(currentThread().getName());
return event;
}
@Override
public Publisher<Event> apply(Publisher<Event> publisher) {
if (getProcessingType() == CPU_LITE_ASYNC) {
return from(publisher).transform(processorPublisher -> Processor.super.apply(publisher))
.publishOn(fromExecutorService(custom));
} else {
return Processor.super.apply(publisher);
}
}
}
public static Matcher<Integer> between(int min, int max) {
return allOf(greaterThanOrEqualTo(min), lessThanOrEqualTo(max));
}
public static Matcher<Long> between(long min, long max) {
return allOf(greaterThanOrEqualTo(min), lessThanOrEqualTo(max));
}
protected void expectRejected() {
expectedException.expect(MessagingException.class);
expectedException.expect(overloadErrorTypeMatcher());
expectedException.expectCause(instanceOf(RejectedExecutionException.class));
}
private TypeSafeMatcher<MessagingException> overloadErrorTypeMatcher() {
return new TypeSafeMatcher<MessagingException>() {
private String errorTypeId;
@Override
public void describeTo(org.hamcrest.Description description) {
description.appendValue(errorTypeId);
}
@Override
protected boolean matchesSafely(MessagingException item) {
errorTypeId = item.getEvent().getError().get().getErrorType().getIdentifier();
return OVERLOAD_ERROR_IDENTIFIER.equals(errorTypeId);
}
};
}
}