/* * Copyright 2001-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.ip.tcp; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.Lifecycle; import org.springframework.context.SmartLifecycle; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.MessageTimeoutException; import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.integration.ip.IpHeaders; import org.springframework.integration.ip.tcp.connection.AbstractClientConnectionFactory; import org.springframework.integration.ip.tcp.connection.AbstractConnectionFactory; import org.springframework.integration.ip.tcp.connection.TcpConnection; import org.springframework.integration.ip.tcp.connection.TcpConnectionFailedCorrelationEvent; import org.springframework.integration.ip.tcp.connection.TcpListener; import org.springframework.integration.ip.tcp.connection.TcpSender; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessagingException; import org.springframework.messaging.support.ErrorMessage; import org.springframework.util.Assert; /** * TCP outbound gateway that uses a client connection factory. If the factory is configured * for single-use connections, each request is sent on a new connection; if the factory does not use * single use connections, each request is blocked until the previous response is received * (or times out). Asynchronous requests/responses over the same connection are not * supported - use a pair of outbound/inbound adapters for that use case. * <p> * {@link SmartLifecycle} methods delegate to the underlying {@link AbstractConnectionFactory} * * * @author Gary Russell * @since 2.0 */ public class TcpOutboundGateway extends AbstractReplyProducingMessageHandler implements TcpSender, TcpListener, Lifecycle { private volatile AbstractClientConnectionFactory connectionFactory; private volatile boolean isSingleUse; private final Map<String, AsyncReply> pendingReplies = new ConcurrentHashMap<String, AsyncReply>(); private final Semaphore semaphore = new Semaphore(1, true); private volatile Expression remoteTimeoutExpression = new LiteralExpression("10000"); private volatile long requestTimeout = 10000; private volatile EvaluationContext evaluationContext = new StandardEvaluationContext(); /** * @param requestTimeout the requestTimeout to set */ public void setRequestTimeout(long requestTimeout) { this.requestTimeout = requestTimeout; } /** * @param remoteTimeout the remoteTimeout to set */ public void setRemoteTimeout(long remoteTimeout) { this.remoteTimeoutExpression = new LiteralExpression("" + remoteTimeout); } /** * @param remoteTimeoutExpression the remoteTimeoutExpression to set */ public void setRemoteTimeoutExpression(Expression remoteTimeoutExpression) { this.remoteTimeoutExpression = remoteTimeoutExpression; } public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) { this.evaluationContext = evaluationContext; } @Override protected void doInit() { super.doInit(); if (this.evaluationContext == null) { this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory()); } } @Override protected Object handleRequestMessage(Message<?> requestMessage) { Assert.notNull(this.connectionFactory, this.getClass().getName() + " requires a client connection factory"); boolean haveSemaphore = false; TcpConnection connection = null; String connectionId = null; try { if (!this.isSingleUse) { logger.debug("trying semaphore"); if (!this.semaphore.tryAcquire(this.requestTimeout, TimeUnit.MILLISECONDS)) { throw new MessageTimeoutException(requestMessage, "Timed out waiting for connection"); } haveSemaphore = true; if (logger.isDebugEnabled()) { logger.debug("got semaphore"); } } connection = this.connectionFactory.getConnection(); AsyncReply reply = new AsyncReply(this.remoteTimeoutExpression.getValue(this.evaluationContext, requestMessage, Long.class)); connectionId = connection.getConnectionId(); this.pendingReplies.put(connectionId, reply); if (logger.isDebugEnabled()) { logger.debug("Added pending reply " + connectionId); } connection.send(requestMessage); Message<?> replyMessage = reply.getReply(); if (replyMessage == null) { if (logger.isDebugEnabled()) { logger.debug("Remote Timeout on " + connectionId); } // The connection is dirty - force it closed. this.connectionFactory.forceClose(connection); throw new MessageTimeoutException(requestMessage, "Timed out waiting for response"); } if (logger.isDebugEnabled()) { logger.debug("Response " + replyMessage); } return replyMessage; } catch (Exception e) { logger.error("Tcp Gateway exception", e); if (e instanceof MessagingException) { throw (MessagingException) e; } throw new MessagingException("Failed to send or receive", e); } finally { if (connectionId != null) { this.pendingReplies.remove(connectionId); if (logger.isDebugEnabled()) { logger.debug("Removed pending reply " + connectionId); } if (this.isSingleUse) { connection.close(); } } if (haveSemaphore) { this.semaphore.release(); if (logger.isDebugEnabled()) { logger.debug("released semaphore"); } } } } @Override public boolean onMessage(Message<?> message) { String connectionId = (String) message.getHeaders().get(IpHeaders.CONNECTION_ID); if (connectionId == null) { logger.error("Cannot correlate response - no connection id"); publishNoConnectionEvent(message, null, "Cannot correlate response - no connection id"); return false; } if (logger.isTraceEnabled()) { logger.trace("onMessage: " + connectionId + "(" + message + ")"); } AsyncReply reply = this.pendingReplies.get(connectionId); if (reply == null) { if (message instanceof ErrorMessage) { /* * Socket errors are sent here so they can be conveyed to any waiting thread. * If there's not one, simply ignore. */ return false; } else { String errorMessage = "Cannot correlate response - no pending reply for " + connectionId; logger.error(errorMessage); publishNoConnectionEvent(message, connectionId, errorMessage); return false; } } reply.setReply(message); return false; } private void publishNoConnectionEvent(Message<?> message, String connectionId, String errorMessage) { ApplicationEventPublisher applicationEventPublisher = this.connectionFactory.getApplicationEventPublisher(); if (applicationEventPublisher != null) { applicationEventPublisher.publishEvent(new TcpConnectionFailedCorrelationEvent(this, connectionId, new MessagingException(message, errorMessage))); } } public void setConnectionFactory(AbstractClientConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; connectionFactory.registerListener(this); connectionFactory.registerSender(this); this.isSingleUse = connectionFactory.isSingleUse(); } @Override public void addNewConnection(TcpConnection connection) { // do nothing - no asynchronous multiplexing supported } @Override public void removeDeadConnection(TcpConnection connection) { // do nothing - no asynchronous multiplexing supported } /** * Specify the Spring Integration reply channel. If this property is not * set the gateway will check for a 'replyChannel' header on the request. * @param replyChannel The reply channel. */ public void setReplyChannel(MessageChannel replyChannel) { this.setOutputChannel(replyChannel); } @Override public String getComponentType() { return "ip:tcp-outbound-gateway"; } @Override public void start() { this.connectionFactory.start(); } @Override public void stop() { this.connectionFactory.stop(); } @Override public boolean isRunning() { return this.connectionFactory.isRunning(); } /** * @return the connectionFactory */ protected AbstractConnectionFactory getConnectionFactory() { return this.connectionFactory; } /** * Class used to coordinate the asynchronous reply to its request. * * @author Gary Russell * @since 2.0 */ private final class AsyncReply { private final CountDownLatch latch; private final CountDownLatch secondChanceLatch; private final long remoteTimeout; private volatile Message<?> reply; private AsyncReply(long remoteTimeout) { this.latch = new CountDownLatch(1); this.secondChanceLatch = new CountDownLatch(1); this.remoteTimeout = remoteTimeout; } /** * Sender blocks here until the reply is received, or we time out * @return The return message or null if we time out * @throws Exception */ public Message<?> getReply() throws Exception { try { if (!this.latch.await(this.remoteTimeout, TimeUnit.MILLISECONDS)) { return null; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } boolean waitForMessageAfterError = true; while (this.reply instanceof ErrorMessage) { if (waitForMessageAfterError) { /* * Possible race condition with NIO; we might have received the close * before the reply, on a different thread. */ logger.debug("second chance"); this.secondChanceLatch.await(2, TimeUnit.SECONDS); // NOSONAR don't care about result waitForMessageAfterError = false; } else if (this.reply.getPayload() instanceof MessagingException) { throw (MessagingException) this.reply.getPayload(); } else { throw new MessagingException("Exception while awaiting reply", (Throwable) this.reply.getPayload()); } } return this.reply; } /** * We have a race condition when a socket is closed right after the reply is received. The close "error" * might arrive before the actual reply. Overwrite an error with a good reply, but not vice-versa. * @param reply the reply message. */ public void setReply(Message<?> reply) { if (this.reply == null) { this.reply = reply; this.latch.countDown(); } else if (this.reply instanceof ErrorMessage) { this.reply = reply; this.secondChanceLatch.countDown(); } } } }