/* * 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.ip.tcp.connection; import java.util.Iterator; import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; import javax.net.ssl.SSLSession; import org.springframework.core.serializer.Deserializer; import org.springframework.core.serializer.Serializer; import org.springframework.integration.ip.IpHeaders; import org.springframework.integration.support.AbstractIntegrationMessageBuilder; import org.springframework.messaging.Message; import org.springframework.util.Assert; /** * Given a list of connection factories, serves up {@link TcpConnection}s * that can iterate over a connection from each factory until the write * succeeds or the list is exhausted. * @author Gary Russell * @since 2.2 * */ public class FailoverClientConnectionFactory extends AbstractClientConnectionFactory { private final List<AbstractClientConnectionFactory> factories; public FailoverClientConnectionFactory(List<AbstractClientConnectionFactory> factories) { super("", 0); Assert.notEmpty(factories, "At least one factory is required"); this.factories = factories; } @Override protected void onInit() throws Exception { super.onInit(); for (AbstractClientConnectionFactory factory : this.factories) { Assert.state(!(this.isSingleUse() ^ factory.isSingleUse()), "Inconsistent singleUse - delegate factories must match this one"); factory.enableManualListenerRegistration(); } } /** * Delegate TCP Client Connection factories that are used to receive * data need a Listener to send the messages to. * This applies to client factories used for outbound gateways * or for a pair of collaborating channel adapters. * <p> * During initialization, if a factory detects it has no listener * it's listening logic (active thread) is terminated. * <p> * The listener registered with a factory is provided to each * connection it creates so it can call the onMessage() method. * <p> * This code satisfies the first requirement in that this * listener signals to the factory that it needs to run * its listening logic. * <p> * When we wrap actual connections with FailoverTcpConnections, * the connection is given the wrapper as a listener, so it * can enhance the headers in onMessage(); the wrapper then invokes * the real listener supplied here, with the modified message. */ @Override public void registerListener(TcpListener listener) { super.registerListener(listener); } @Override public void registerSender(TcpSender sender) { for (AbstractClientConnectionFactory factory : this.factories) { factory.registerSender(sender); } } @Override protected TcpConnectionSupport obtainConnection() throws Exception { TcpConnectionSupport connection = this.getTheConnection(); if (connection != null && connection.isOpen()) { ((FailoverTcpConnection) connection).incrementEpoch(); return connection; } FailoverTcpConnection failoverTcpConnection = new FailoverTcpConnection(this.factories); if (getListener() != null) { failoverTcpConnection.registerListener(getListener()); } failoverTcpConnection.incrementEpoch(); return failoverTcpConnection; } @Override public void start() { for (AbstractClientConnectionFactory factory : this.factories) { factory.enableManualListenerRegistration(); factory.start(); } this.setActive(true); super.start(); } @Override public void stop() { this.setActive(false); for (AbstractClientConnectionFactory factory : this.factories) { factory.stop(); } } /** * Returns true if all factories are running */ @Override public boolean isRunning() { boolean isRunning = true; for (AbstractClientConnectionFactory factory : this.factories) { isRunning = !isRunning ? false : factory.isRunning(); } return isRunning; } /** * Wrapper for a list of factories; delegates to a connection from * one of those factories and fails over to another if necessary. * @author Gary Russell * @since 2.2 * */ private final class FailoverTcpConnection extends TcpConnectionSupport implements TcpListener { private final List<AbstractClientConnectionFactory> factories; private final String connectionId; private volatile Iterator<AbstractClientConnectionFactory> factoryIterator; private volatile AbstractClientConnectionFactory currentFactory; private volatile TcpConnectionSupport delegate; private volatile boolean open = true; private final AtomicLong epoch = new AtomicLong(); private FailoverTcpConnection(List<AbstractClientConnectionFactory> factories) throws Exception { this.factories = factories; this.factoryIterator = factories.iterator(); findAConnection(); this.connectionId = UUID.randomUUID().toString(); } void incrementEpoch() { this.epoch.incrementAndGet(); } /** * Finds a connection from the underlying list of factories. If necessary, * each factory is tried; including the current one if we wrap around. * This allows for the condition where the current connection is closed, * the current factory can serve up a new connection, but all other * factories are down. * @throws Exception if an exception occurs */ private synchronized void findAConnection() throws Exception { boolean success = false; AbstractClientConnectionFactory lastFactoryToTry = this.currentFactory; AbstractClientConnectionFactory nextFactory = null; if (!this.factoryIterator.hasNext()) { this.factoryIterator = this.factories.iterator(); } boolean retried = false; while (!success) { try { nextFactory = this.factoryIterator.next(); this.delegate = nextFactory.getConnection(); if (logger.isDebugEnabled()) { logger.debug("Got " + this.delegate.getConnectionId() + " from " + nextFactory); } this.delegate.registerListener(this); this.currentFactory = nextFactory; success = this.delegate.isOpen(); } catch (Exception e) { if (logger.isDebugEnabled()) { logger.debug(nextFactory + " failed with " + e.toString() + ", trying another"); } if (!this.factoryIterator.hasNext()) { if (retried && lastFactoryToTry == null || lastFactoryToTry == nextFactory) { /* * We've tried every factory including the * one the current connection was on. */ this.open = false; throw e; } this.factoryIterator = this.factories.iterator(); retried = true; } } } } @Override public void close() { this.delegate.close(); this.open = false; } @Override public boolean isOpen() { return this.open; } /** * Sends to the current connection; if it fails, attempts to * send to a new connection obtained from {@link #findAConnection()}. * If send fails on a connection from every factory, we give up. */ @Override public synchronized void send(Message<?> message) throws Exception { boolean success = false; AbstractClientConnectionFactory lastFactoryToTry = this.currentFactory; AbstractClientConnectionFactory lastFactoryTried = null; boolean retried = false; while (!success) { try { lastFactoryTried = this.currentFactory; this.delegate.send(message); success = true; } catch (Exception e) { if (retried && lastFactoryTried == lastFactoryToTry) { logger.error("All connection factories exhausted", e); this.open = false; throw e; } retried = true; if (logger.isDebugEnabled()) { logger.debug("Send to " + this.delegate.getConnectionId() + " failed; attempting failover", e); } this.delegate.close(); findAConnection(); if (logger.isDebugEnabled()) { logger.debug("Failing over to " + this.delegate.getConnectionId()); } } } } @Override public Object getPayload() throws Exception { return this.delegate.getPayload(); } @Override public void run() { throw new UnsupportedOperationException("Not supported on FailoverTcpConnection"); } @Override public String getHostName() { return this.delegate.getHostName(); } @Override public String getHostAddress() { return this.delegate.getHostAddress(); } @Override public int getPort() { return this.delegate.getPort(); } @Override public Object getDeserializerStateKey() { return this.delegate.getDeserializerStateKey(); } @Override public void registerSender(TcpSender sender) { this.delegate.registerSender(sender); } @Override public String getConnectionId() { return this.connectionId + ":" + this.epoch; } @Override public SocketInfo getSocketInfo() { return this.delegate.getSocketInfo(); } @Override public boolean isServer() { return this.delegate.isServer(); } @Override public void setMapper(TcpMessageMapper mapper) { this.delegate.setMapper(mapper); } @Override public Deserializer<?> getDeserializer() { return this.delegate.getDeserializer(); } @Override public void setDeserializer(Deserializer<?> deserializer) { this.delegate.setDeserializer(deserializer); } @Override public Serializer<?> getSerializer() { return this.delegate.getSerializer(); } @Override public void setSerializer(Serializer<?> serializer) { this.delegate.setSerializer(serializer); } @Override public SSLSession getSslSession() { return this.delegate.getSslSession(); } /** * We have to intercept the message to replace the connectionId header with * ours so the listener can correlate a response with a request. We supply * the actual connectionId in another header for convenience and tracing * purposes. */ @Override public boolean onMessage(Message<?> message) { if (this.delegate.getConnectionId().equals(message.getHeaders().get(IpHeaders.CONNECTION_ID))) { AbstractIntegrationMessageBuilder<?> messageBuilder = FailoverClientConnectionFactory.this .getMessageBuilderFactory().fromMessage(message) .setHeader(IpHeaders.CONNECTION_ID, this.getConnectionId()); if (message.getHeaders().get(IpHeaders.ACTUAL_CONNECTION_ID) == null) { messageBuilder.setHeader(IpHeaders.ACTUAL_CONNECTION_ID, message.getHeaders().get(IpHeaders.CONNECTION_ID)); } return this.getListener().onMessage(messageBuilder.build()); } else { if (logger.isDebugEnabled()) { logger.debug("Message from defunct connection ignored " + message); } return false; } } } }