/*
* Copyright 2015-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.aggregator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.integration.handler.DiscardingMessageHandler;
import org.springframework.integration.handler.MessageTriggerAction;
import org.springframework.integration.store.SimpleMessageGroup;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
/**
* A message handler that suspends the thread until a message with corresponding
* correlation is passed into the {@link #trigger(Message) trigger} method or
* the timeout occurs. Only one thread with a particular correlation (result of invoking
* the {@link CorrelationStrategy}) can be suspended at a time. If the inbound thread does
* not arrive before the trigger thread, the latter is suspended until it does, or the
* timeout occurs.
* <p>
* The default {@link CorrelationStrategy} is a {@link HeaderAttributeCorrelationStrategy}.
* <p>
* The default output processor is a {@link DefaultAggregatingMessageGroupProcessor}.
*
* @author Gary Russell
*
* @since 4.2
*/
public class BarrierMessageHandler extends AbstractReplyProducingMessageHandler
implements MessageTriggerAction, DiscardingMessageHandler {
private final Map<Object, SynchronousQueue<Message<?>>> suspensions = new ConcurrentHashMap<>();
private final Map<Object, Thread> inProcess = new ConcurrentHashMap<>();
private final long timeout;
private final CorrelationStrategy correlationStrategy;
private final MessageGroupProcessor messageGroupProcessor;
private volatile MessageChannel discardChannel;
private String discardChannelName;
/**
* Construct an instance with the provided timeout and default correlation and
* output strategies.
* @param timeout the timeout in milliseconds.
*/
public BarrierMessageHandler(long timeout) {
this(timeout, new DefaultAggregatingMessageGroupProcessor());
}
/**
* Construct an instance with the provided timeout and output processor, and default
* correlation strategy.
* @param timeout the timeout in milliseconds.
* @param outputProcessor the output {@link MessageGroupProcessor}.
*/
public BarrierMessageHandler(long timeout, MessageGroupProcessor outputProcessor) {
this(timeout, outputProcessor,
new HeaderAttributeCorrelationStrategy(IntegrationMessageHeaderAccessor.CORRELATION_ID)
);
}
/**
* Construct an instance with the provided timeout and correlation strategy, and default
* output processor.
* @param timeout the timeout in milliseconds.
* @param correlationStrategy the correlation strategy.
*/
public BarrierMessageHandler(long timeout, CorrelationStrategy correlationStrategy) {
this(timeout, new DefaultAggregatingMessageGroupProcessor(), correlationStrategy);
}
/**
* Construct an instance with the provided timeout and output processor, and default
* correlation strategy.
* @param timeout the timeout in milliseconds.
* @param outputProcessor the output {@link MessageGroupProcessor}.
* @param correlationStrategy the correlation strategy.
*/
public BarrierMessageHandler(long timeout, MessageGroupProcessor outputProcessor,
CorrelationStrategy correlationStrategy) {
Assert.notNull(outputProcessor, "'messageGroupProcessor' cannot be null");
Assert.notNull(correlationStrategy, "'correlationStrategy' cannot be null");
this.messageGroupProcessor = outputProcessor;
this.correlationStrategy = correlationStrategy;
this.timeout = timeout;
}
/**
* Set the name of the channel to which late arriving trigger messages are sent.
* @param discardChannelName the discard channel.
* @since 5.0
*/
public void setDiscardChannelName(String discardChannelName) {
this.discardChannelName = discardChannelName;
}
/**
* Set the channel to which late arriving trigger messages are sent.
* @param discardChannel the discard channel.
* @since 5.0
*/
public void setDiscardChannel(MessageChannel discardChannel) {
this.discardChannel = discardChannel;
}
/**
* @since 5.0
*/
@Override
public MessageChannel getDiscardChannel() {
if (this.discardChannel == null && this.discardChannelName != null && getChannelResolver() != null) {
this.discardChannel = getChannelResolver().resolveDestination(this.discardChannelName);
}
return this.discardChannel;
}
@Override
public String getComponentType() {
return "barrier";
}
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
Object key = this.correlationStrategy.getCorrelationKey(requestMessage);
if (key == null) {
throw new MessagingException(requestMessage, "Correlation Strategy returned null");
}
Thread existing = this.inProcess.putIfAbsent(key, Thread.currentThread());
if (existing != null) {
throw new MessagingException(requestMessage, "Correlation key ("
+ key + ") is already in use by " + existing.getName());
}
SynchronousQueue<Message<?>> syncQueue = createOrObtainQueue(key);
try {
Message<?> releaseMessage = syncQueue.poll(this.timeout, TimeUnit.MILLISECONDS);
if (releaseMessage != null) {
return processRelease(key, requestMessage, releaseMessage);
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MessageHandlingException(requestMessage, "Interrupted while waiting for release", e);
}
finally {
this.inProcess.remove(key);
this.suspensions.remove(key);
}
return null;
}
private Object processRelease(Object key, Message<?> requestMessage, Message<?> releaseMessage) {
this.suspensions.remove(key);
if (releaseMessage.getPayload() instanceof Throwable) {
throw new MessagingException(requestMessage, "Releasing flow returned a throwable",
(Throwable) releaseMessage.getPayload());
}
else {
return buildResult(key, requestMessage, releaseMessage);
}
}
/**
* Override to change the default mechanism by which the inbound and release messages
* are returned as a result.
* @param key The correlation key.
* @param requestMessage the inbound message.
* @param releaseMessage the release message.
* @return the result.
*/
protected Object buildResult(Object key, Message<?> requestMessage, Message<?> releaseMessage) {
SimpleMessageGroup group = new SimpleMessageGroup(key);
group.add(requestMessage);
group.add(releaseMessage);
return this.messageGroupProcessor.processMessageGroup(group);
}
private SynchronousQueue<Message<?>> createOrObtainQueue(Object key) {
SynchronousQueue<Message<?>> syncQueue = new SynchronousQueue<>();
SynchronousQueue<Message<?>> existing = this.suspensions.putIfAbsent(key, syncQueue);
if (existing != null) {
syncQueue = existing;
}
return syncQueue;
}
@Override
public void trigger(Message<?> message) {
Object key = this.correlationStrategy.getCorrelationKey(message);
if (key == null) {
throw new MessagingException(message, "Correlation Strategy returned null");
}
SynchronousQueue<Message<?>> syncQueue = createOrObtainQueue(key);
try {
if (!syncQueue.offer(message, this.timeout, TimeUnit.MILLISECONDS)) {
this.logger.error("Suspending thread timed out or did not arrive within timeout for: " + message);
this.suspensions.remove(key);
if (getDiscardChannel() != null) {
this.messagingTemplate.send(getDiscardChannel(), message);
}
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
this.logger.error("Interrupted while waiting for the suspending thread for: " + message);
this.suspensions.remove(key);
}
}
}