/*
* Copyright 2002-2016 the original author or authors.
*
* 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 org.springframework.integration.handler;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import java.util.Calendar;
import java.util.Date;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.MessagePublishingErrorHandler;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.store.MessageGroupStore;
import org.springframework.integration.store.SimpleMessageStore;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* @author Mark Fisher
* @author Artem Bilan
* @author Gunnar Hillert
* @author Gary Russell
* @since 1.0.3
*/
public class DelayHandlerTests {
private static final String DELAYER_MESSAGE_GROUP_ID = "testDelayer.messageGroupId";
private final DirectChannel input = new DirectChannel();
private final DirectChannel output = new DirectChannel();
private final CountDownLatch latch = new CountDownLatch(1);
private ThreadPoolTaskScheduler taskScheduler;
private DelayHandler delayHandler;
private final ResultHandler resultHandler = new ResultHandler();
@Before
public void setup() {
input.setBeanName("input");
output.setBeanName("output");
taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.afterPropertiesSet();
delayHandler = new DelayHandler(DELAYER_MESSAGE_GROUP_ID, taskScheduler);
delayHandler.setOutputChannel(output);
delayHandler.setBeanFactory(mock(BeanFactory.class));
input.subscribe(delayHandler);
output.subscribe(resultHandler);
}
@After
public void tearDown() {
taskScheduler.destroy();
}
private void setDelayExpression() {
Expression expression = new SpelExpressionParser().parseExpression("headers.delay");
this.delayHandler.setDelayExpression(expression);
}
private void startDelayerHandler() {
delayHandler.afterPropertiesSet();
delayHandler.onApplicationEvent(new ContextRefreshedEvent(TestUtils.createTestApplicationContext()));
}
@Test
public void noDelayHeaderAndDefaultDelayIsZero() throws Exception {
this.startDelayerHandler();
Message<?> message = MessageBuilder.withPayload("test").build();
input.send(message);
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
assertSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test
public void noDelayHeaderAndDefaultDelayIsPositive() throws Exception {
delayHandler.setDefaultDelay(10);
this.startDelayerHandler();
Message<?> message = MessageBuilder.withPayload("test").build();
input.send(message);
waitForLatch(10000);
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test
public void delayHeaderAndDefaultDelayWouldTimeout() throws Exception {
delayHandler.setDefaultDelay(5000);
this.setDelayExpression();
this.startDelayerHandler();
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", 100).build();
input.send(message);
waitForLatch(10000);
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test
public void delayHeaderIsNegativeAndDefaultDelayWouldTimeout() throws Exception {
delayHandler.setDefaultDelay(5000);
this.setDelayExpression();
this.startDelayerHandler();
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", -7000).build();
input.send(message);
waitForLatch(10000);
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
assertSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test
public void delayHeaderIsInvalidFallsBackToDefaultDelay() throws Exception {
delayHandler.setDefaultDelay(5);
this.setDelayExpression();
this.startDelayerHandler();
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", "not a number").build();
input.send(message);
waitForLatch(10000);
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test
public void delayHeaderIsDateInTheFutureAndDefaultDelayWouldTimeout() throws Exception {
delayHandler.setDefaultDelay(5000);
this.setDelayExpression();
this.startDelayerHandler();
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", new Date(new Date().getTime() + 150)).build();
input.send(message);
waitForLatch(10000);
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test
public void delayHeaderIsDateInThePastAndDefaultDelayWouldTimeout() throws Exception {
delayHandler.setDefaultDelay(5000);
this.setDelayExpression();
this.startDelayerHandler();
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", new Date(new Date().getTime() - 60 * 1000)).build();
input.send(message);
waitForLatch(10000);
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
assertSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test
public void delayHeaderIsNullDateAndDefaultDelayIsZero() throws Exception {
this.setDelayExpression();
this.startDelayerHandler();
Date nullDate = null;
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", nullDate).build();
input.send(message);
waitForLatch(10000);
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
assertSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test(expected = TestTimedOutException.class)
public void delayHeaderIsFutureDateAndTimesOut() throws Exception {
this.setDelayExpression();
this.startDelayerHandler();
Date future = new Date(new Date().getTime() + 60 * 1000);
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", future).build();
input.send(message);
waitForLatch(100);
}
@Test
public void delayHeaderIsValidStringAndDefaultDelayWouldTimeout() throws Exception {
delayHandler.setDefaultDelay(5000);
this.setDelayExpression();
this.startDelayerHandler();
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", "20").build();
input.send(message);
waitForLatch(10000);
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test
public void verifyShutdownWithoutWaitingByDefault() throws Exception {
delayHandler.setDefaultDelay(5000);
this.startDelayerHandler();
delayHandler.handleMessage(new GenericMessage<String>("foo"));
taskScheduler.destroy();
final CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
try {
taskScheduler.getScheduledExecutor().awaitTermination(10000, TimeUnit.MILLISECONDS);
latch.countDown();
}
catch (InterruptedException e) {
// won't countDown
}
}).start();
assertTrue(latch.await(10, TimeUnit.SECONDS));
}
@Test
public void verifyShutdownWithWait() throws Exception {
delayHandler.setDefaultDelay(5000);
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
this.startDelayerHandler();
delayHandler.handleMessage(new GenericMessage<String>("foo"));
taskScheduler.destroy();
final CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
try {
taskScheduler.getScheduledExecutor().awaitTermination(10000, TimeUnit.MILLISECONDS);
latch.countDown();
}
catch (InterruptedException e) {
// won't countDown
}
}).start();
latch.await(50, TimeUnit.MILLISECONDS);
assertEquals(1, latch.getCount());
}
@Test(expected = MessageDeliveryException.class)
public void handlerThrowsExceptionWithNoDelay() throws Exception {
this.startDelayerHandler();
output.unsubscribe(resultHandler);
output.subscribe(message -> {
throw new UnsupportedOperationException("intentional test failure");
});
Message<?> message = MessageBuilder.withPayload("test").build();
input.send(message);
}
@Test
public void errorChannelHeaderAndHandlerThrowsExceptionWithDelay() throws Exception {
DirectChannel errorChannel = new DirectChannel();
MessagePublishingErrorHandler errorHandler = new MessagePublishingErrorHandler();
errorHandler.setDefaultErrorChannel(errorChannel);
taskScheduler.setErrorHandler(errorHandler);
this.setDelayExpression();
this.startDelayerHandler();
output.unsubscribe(resultHandler);
errorChannel.subscribe(resultHandler);
output.subscribe(message -> {
throw new UnsupportedOperationException("intentional test failure");
});
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", "10")
.setErrorChannel(errorChannel).build();
input.send(message);
waitForLatch(10000);
Message<?> errorMessage = resultHandler.lastMessage;
assertEquals(MessageDeliveryException.class, errorMessage.getPayload().getClass());
MessageDeliveryException exceptionPayload = (MessageDeliveryException) errorMessage.getPayload();
assertSame(message.getPayload(), exceptionPayload.getFailedMessage().getPayload());
assertEquals(UnsupportedOperationException.class, exceptionPayload.getCause().getClass());
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test
public void errorChannelNameHeaderAndHandlerThrowsExceptionWithDelay() throws Exception {
String errorChannelName = "customErrorChannel";
StaticApplicationContext context = new StaticApplicationContext();
context.registerSingleton(errorChannelName, DirectChannel.class);
context.registerSingleton(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, DirectChannel.class);
context.refresh();
DirectChannel customErrorChannel = (DirectChannel) context.getBean(errorChannelName);
MessagePublishingErrorHandler errorHandler = new MessagePublishingErrorHandler();
errorHandler.setBeanFactory(context);
taskScheduler.setErrorHandler(errorHandler);
this.setDelayExpression();
this.startDelayerHandler();
output.unsubscribe(resultHandler);
customErrorChannel.subscribe(resultHandler);
output.subscribe(message -> {
throw new UnsupportedOperationException("intentional test failure");
});
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", "10")
.setErrorChannelName(errorChannelName).build();
input.send(message);
waitForLatch(10000);
Message<?> errorMessage = resultHandler.lastMessage;
assertEquals(MessageDeliveryException.class, errorMessage.getPayload().getClass());
MessageDeliveryException exceptionPayload = (MessageDeliveryException) errorMessage.getPayload();
assertSame(message.getPayload(), exceptionPayload.getFailedMessage().getPayload());
assertEquals(UnsupportedOperationException.class, exceptionPayload.getCause().getClass());
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test
public void defaultErrorChannelAndHandlerThrowsExceptionWithDelay() throws Exception {
StaticApplicationContext context = new StaticApplicationContext();
context.registerSingleton(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, DirectChannel.class);
context.refresh();
DirectChannel defaultErrorChannel = (DirectChannel) context.getBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME);
MessagePublishingErrorHandler errorHandler = new MessagePublishingErrorHandler();
errorHandler.setBeanFactory(context);
taskScheduler.setErrorHandler(errorHandler);
this.setDelayExpression();
this.startDelayerHandler();
output.unsubscribe(resultHandler);
defaultErrorChannel.subscribe(resultHandler);
output.subscribe(message -> {
throw new UnsupportedOperationException("intentional test failure");
});
Message<?> message = MessageBuilder.withPayload("test")
.setHeader("delay", "10").build();
input.send(message);
waitForLatch(10000);
Message<?> errorMessage = resultHandler.lastMessage;
assertEquals(MessageDeliveryException.class, errorMessage.getPayload().getClass());
MessageDeliveryException exceptionPayload = (MessageDeliveryException) errorMessage.getPayload();
assertSame(message.getPayload(), exceptionPayload.getFailedMessage().getPayload());
assertEquals(UnsupportedOperationException.class, exceptionPayload.getCause().getClass());
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
}
@Test //INT-1132
public void testReschedulePersistedMessagesOnStartup() throws Exception {
MessageGroupStore messageGroupStore = new SimpleMessageStore();
this.delayHandler.setDefaultDelay(2000);
this.delayHandler.setMessageStore(messageGroupStore);
this.startDelayerHandler();
Message<?> message = MessageBuilder.withPayload("test").build();
this.input.send(message);
Thread.sleep(100);
// emulate restart
this.taskScheduler.destroy();
assertEquals(1, messageGroupStore.getMessageGroupCount());
assertEquals(DELAYER_MESSAGE_GROUP_ID, messageGroupStore.iterator().next().getGroupId());
assertEquals(1, messageGroupStore.messageGroupSize(DELAYER_MESSAGE_GROUP_ID));
assertEquals(1, messageGroupStore.getMessageCountForAllMessageGroups());
MessageGroup messageGroup = messageGroupStore.getMessageGroup(DELAYER_MESSAGE_GROUP_ID);
Message<?> messageInStore = messageGroup.getMessages().iterator().next();
Object payload = messageInStore.getPayload();
assertEquals("DelayedMessageWrapper", payload.getClass().getSimpleName());
assertEquals(message.getPayload(), TestUtils.getPropertyValue(payload, "original.payload"));
this.taskScheduler.afterPropertiesSet();
this.delayHandler = new DelayHandler(DELAYER_MESSAGE_GROUP_ID, this.taskScheduler);
this.delayHandler.setOutputChannel(output);
this.delayHandler.setDefaultDelay(200);
this.delayHandler.setMessageStore(messageGroupStore);
this.delayHandler.setBeanFactory(mock(BeanFactory.class));
this.startDelayerHandler();
waitForLatch(10000);
assertSame(message.getPayload(), this.resultHandler.lastMessage.getPayload());
assertNotSame(Thread.currentThread(), this.resultHandler.lastThread);
assertEquals(1, messageGroupStore.getMessageGroupCount());
assertEquals(0, messageGroupStore.messageGroupSize(DELAYER_MESSAGE_GROUP_ID));
}
@Test //INT-1132
// Can happen in the parent-child context e.g. Spring-MVC applications
public void testDoubleOnApplicationEvent() throws Exception {
this.delayHandler = Mockito.spy(this.delayHandler);
Mockito.doAnswer(invocation -> null).when(this.delayHandler).reschedulePersistedMessages();
ContextRefreshedEvent contextRefreshedEvent = new ContextRefreshedEvent(TestUtils.createTestApplicationContext());
this.delayHandler.onApplicationEvent(contextRefreshedEvent);
this.delayHandler.onApplicationEvent(contextRefreshedEvent);
Mockito.verify(this.delayHandler, Mockito.times(1)).reschedulePersistedMessages();
}
@Test(expected = MessageHandlingException.class)
public void testInt2243IgnoreExpressionFailuresAsFalse() throws Exception {
this.setDelayExpression();
this.delayHandler.setIgnoreExpressionFailures(false);
this.startDelayerHandler();
this.delayHandler.handleMessage(new GenericMessage<String>("test"));
}
@Test //INT-3560
/*
It's difficult to test it from real ctx, because any async process from 'inbound-channel-adapter'
can't achieve the DelayHandler before the main thread emits 'ContextRefreshedEvent'.
*/
public void testRescheduleAndHandleAtTheSameTime() throws Exception {
QueueChannel results = new QueueChannel();
delayHandler.setOutputChannel(results);
this.delayHandler.setDefaultDelay(100);
startDelayerHandler();
this.input.send(new GenericMessage<>("foo"));
this.delayHandler.reschedulePersistedMessages();
Message<?> message = results.receive(10000);
assertNotNull(message);
message = results.receive(500);
assertNull(message);
}
@Test
public void testRescheduleForTheDateDelay() throws Exception {
this.delayHandler.setDelayExpression(new SpelExpressionParser().parseExpression("payload"));
this.delayHandler.setOutputChannel(new DirectChannel());
this.delayHandler.setIgnoreExpressionFailures(false);
startDelayerHandler();
Calendar releaseDate = Calendar.getInstance();
releaseDate.add(Calendar.HOUR, 1);
this.delayHandler.handleMessage(new GenericMessage<>(releaseDate.getTime()));
// emulate restart
this.taskScheduler.destroy();
MessageGroupStore messageStore = TestUtils.getPropertyValue(this.delayHandler, "messageStore",
MessageGroupStore.class);
MessageGroup messageGroup = messageStore.getMessageGroup(DELAYER_MESSAGE_GROUP_ID);
Message<?> messageInStore = messageGroup.getMessages().iterator().next();
Object payload = messageInStore.getPayload();
DirectFieldAccessor dfa = new DirectFieldAccessor(payload);
long requestTime = (long) dfa.getPropertyValue("requestDate");
Calendar requestDate = Calendar.getInstance();
requestDate.setTimeInMillis(requestTime);
requestDate.add(Calendar.HOUR, -2);
dfa.setPropertyValue("requestDate", requestDate.getTimeInMillis());
this.taskScheduler.afterPropertiesSet();
this.delayHandler.reschedulePersistedMessages();
Thread.sleep(10);
Queue<?> works = TestUtils.getPropertyValue(this.taskScheduler, "scheduledExecutor.workQueue", Queue.class);
assertEquals(1, works.size());
}
private void waitForLatch(long timeout) {
try {
this.latch.await(timeout, TimeUnit.MILLISECONDS);
if (latch.getCount() != 0) {
throw new TestTimedOutException();
}
}
catch (InterruptedException e) {
throw new RuntimeException("interrupted while waiting for latch");
}
}
private class ResultHandler implements MessageHandler {
private volatile Message<?> lastMessage;
private volatile Thread lastThread;
ResultHandler() {
super();
}
@Override
public void handleMessage(Message<?> message) {
this.lastMessage = message;
this.lastThread = Thread.currentThread();
latch.countDown();
}
}
@SuppressWarnings("serial")
private static class TestTimedOutException extends RuntimeException {
TestTimedOutException() {
super("timed out while waiting for latch");
}
}
}