Java tutorial
/*********************************************************************************************************************** * Copyright (C) 2010-2013 by the Stratosphere project (http://stratosphere.eu) * * 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 eu.stratosphere.nephele.taskmanager.bytebuffered; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.nio.channels.WritableByteChannel; import java.util.ArrayDeque; import java.util.Iterator; import java.util.Queue; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import eu.stratosphere.nephele.io.channels.ChannelID; import eu.stratosphere.nephele.taskmanager.transferenvelope.DefaultSerializer; import eu.stratosphere.nephele.taskmanager.transferenvelope.TransferEnvelope; /** * This class represents an outgoing TCP connection through which {@link TransferEnvelope} objects can be sent. * {@link TransferEnvelope} objects are received from the {@link ByteBufferedChannelManager} and added to a queue. An * additional network thread then takes the envelopes from the queue and transmits them to the respective destination * host. * */ public class OutgoingConnection { /** * The log object used to report debug information and possible errors. */ private static final Log LOG = LogFactory.getLog(OutgoingConnection.class); /** * The address this outgoing connection is connected to. */ private final RemoteReceiver remoteReceiver; /** * The outgoing connection thread which actually transmits the queued transfer envelopes. */ private final OutgoingConnectionThread connectionThread; /** * The queue of transfer envelopes to be transmitted. */ private final Queue<TransferEnvelope> queuedEnvelopes = new ArrayDeque<TransferEnvelope>(); /** * The {@link DefaultSerializer} object used to transform the envelopes into a byte stream. */ private final DefaultSerializer serializer = new DefaultSerializer(); /** * The {@link TransferEnvelope} that is currently processed. */ private TransferEnvelope currentEnvelope = null; /** * Stores whether the underlying TCP connection is established. As this variable is accessed by the byte buffered * channel manager and the outgoing connection thread, it must be protected by a monitor. */ private boolean isConnected = false; /** * Stores whether is underlying TCP connection is subscribed to the NIO write event. As this variable is accessed by * the byte buffered channel and the outgoing connection thread, it must be protected by a monitor. */ private boolean isSubscribedToWriteEvent = false; /** * The overall number of connection retries which shall be performed before a connection error is reported. */ private final int numberOfConnectionRetries; /** * The number of connection retries left before an I/O error is reported. */ private int retriesLeft = 0; /** * The timestamp of the last connection retry. */ private long timstampOfLastRetry = 0; /** * The current selection key representing the interest set of the underlying TCP NIO connection. This variable may * only be accessed the the outgoing connection thread. */ private SelectionKey selectionKey = null; /** * The period of time in milliseconds that shall be waited before a connection attempt is considered to be failed. */ private static long RETRYINTERVAL = 1000L; // 1 second /** * Constructs a new outgoing connection object. * * @param remoteReceiver * the address of the destination host this outgoing connection object is supposed to connect to * @param connectionThread * the connection thread which actually handles the network transfer * @param numberOfConnectionRetries * the number of connection retries allowed before an I/O error is reported */ public OutgoingConnection(RemoteReceiver remoteReceiver, OutgoingConnectionThread connectionThread, int numberOfConnectionRetries) { this.remoteReceiver = remoteReceiver; this.connectionThread = connectionThread; this.numberOfConnectionRetries = numberOfConnectionRetries; } /** * Adds a new {@link TransferEnvelope} to the queue of envelopes to be transmitted to the destination host of this * connection. * <p> * This method should only be called by the {@link ByteBufferedChannelManager} object. * * @param transferEnvelope * the envelope to be added to the transfer queue */ public void queueEnvelope(TransferEnvelope transferEnvelope) { synchronized (this.queuedEnvelopes) { checkConnection(); this.queuedEnvelopes.add(transferEnvelope); } } private void checkConnection() { synchronized (this.queuedEnvelopes) { if (!this.isConnected) { this.retriesLeft = this.numberOfConnectionRetries; this.timstampOfLastRetry = System.currentTimeMillis(); this.connectionThread.triggerConnect(this); this.isConnected = true; this.isSubscribedToWriteEvent = true; } else { if (!this.isSubscribedToWriteEvent) { this.connectionThread.subscribeToWriteEvent(this.selectionKey); this.isSubscribedToWriteEvent = true; } } } } /** * Returns the {@link InetSocketAddress} to the destination host this outgoing connection is supposed to be * connected to. * <p> * This method should be called by the {@link OutgoingConnectionThread} object only. * * @return the {@link InetSocketAddress} to the destination host this outgoing connection is supposed to be * connected to */ public InetSocketAddress getConnectionAddress() { return this.remoteReceiver.getConnectionAddress(); } /** * Reports a problem which occurred while establishing the underlying TCP connection to this outgoing connection * object. Depending on the number of connection retries left, this method will either try to reestablish the TCP * connection or report an I/O error to all tasks which have queued envelopes for this connection. In the latter * case all queued envelopes will be dropped and all included buffers will be freed. * <p> * This method should only be called by the {@link OutgoingConnectionThread} object. * * @param ioe * thrown if an error occurs while reseting the underlying TCP connection */ public void reportConnectionProblem(IOException ioe) { // First, write exception to log final long currentTime = System.currentTimeMillis(); if (currentTime - this.timstampOfLastRetry >= RETRYINTERVAL) { LOG.error("Cannot connect to " + this.remoteReceiver + ", " + this.retriesLeft + " retries left"); } synchronized (this.queuedEnvelopes) { if (this.selectionKey != null) { final SocketChannel socketChannel = (SocketChannel) this.selectionKey.channel(); if (socketChannel != null) { try { socketChannel.close(); } catch (IOException e) { LOG.debug("Error while trying to close the socket channel to " + this.remoteReceiver); } } this.selectionKey.cancel(); this.selectionKey = null; this.isConnected = false; this.isSubscribedToWriteEvent = false; } if (hasRetriesLeft(currentTime)) { this.connectionThread.triggerConnect(this); this.isConnected = true; this.isSubscribedToWriteEvent = true; return; } // Error is fatal LOG.error(ioe); // Notify source of current envelope and release buffer if (this.currentEnvelope != null) { if (this.currentEnvelope.getBuffer() != null) { this.currentEnvelope.getBuffer().recycleBuffer(); this.currentEnvelope = null; } } // Notify all other tasks which are waiting for data to be transmitted final Iterator<TransferEnvelope> iter = this.queuedEnvelopes.iterator(); while (iter.hasNext()) { final TransferEnvelope envelope = iter.next(); iter.remove(); // Recycle the buffer inside the envelope if (envelope.getBuffer() != null) { envelope.getBuffer().recycleBuffer(); } } this.queuedEnvelopes.clear(); } } /** * Reports an I/O error which occurred while writing data to the TCP connection. As a result of the I/O error the * connection is closed and the interest keys are canceled. Moreover, the task which queued the currently * transmitted transfer envelope is notified about the error and the current envelope is dropped. If the current * envelope contains a buffer, the buffer is freed. * <p> * This method should only be called by the {@link OutgoingConnectionThread} object. * * @param ioe * thrown if an error occurs while reseting the connection */ public void reportTransmissionProblem(IOException ioe) { final SocketChannel socketChannel = (SocketChannel) this.selectionKey.channel(); // First, write exception to log if (this.currentEnvelope != null) { LOG.error("The connection between " + socketChannel.socket().getLocalAddress() + " and " + socketChannel.socket().getRemoteSocketAddress() + " experienced an IOException for transfer envelope " + this.currentEnvelope.getSequenceNumber()); } else { LOG.error("The connection between " + socketChannel.socket().getLocalAddress() + " and " + socketChannel.socket().getRemoteSocketAddress() + " experienced an IOException"); } // Close the connection and cancel the interest key synchronized (this.queuedEnvelopes) { try { LOG.debug("Closing connection to " + socketChannel.socket().getRemoteSocketAddress()); socketChannel.close(); } catch (IOException e) { LOG.debug("An error occurred while responding to an IOException"); LOG.debug(e); } this.selectionKey.cancel(); // Error is fatal LOG.error(ioe); // Trigger new connection if there are more envelopes to be transmitted if (this.queuedEnvelopes.isEmpty()) { this.isConnected = false; this.isSubscribedToWriteEvent = false; } else { this.connectionThread.triggerConnect(this); this.isConnected = true; this.isSubscribedToWriteEvent = true; } // We must assume the current envelope is corrupted so we notify the task which created it. if (this.currentEnvelope != null) { if (this.currentEnvelope.getBuffer() != null) { this.currentEnvelope.getBuffer().recycleBuffer(); this.currentEnvelope = null; } } } } /** * Checks whether further retries are left for establishing the underlying TCP connection. * * @param currentTime * the current system time in milliseconds since January 1st, 1970 * @return <code>true</code> if there are retries left, <code>false</code> otherwise */ private boolean hasRetriesLeft(long currentTime) { if (currentTime - this.timstampOfLastRetry >= RETRYINTERVAL) { this.retriesLeft--; this.timstampOfLastRetry = currentTime; if (this.retriesLeft == 0) { return false; } } return true; } /** * Writes the content of the current {@link TransferEnvelope} object to the underlying TCP connection. * <p> * This method should only be called by the {@link OutgoingConnectionThread} object. * * @return <code>true</code> if there is more data from this/other queued envelopes to be written to this channel * @throws IOException * thrown if an error occurs while writing the data to the channel */ public boolean write() throws IOException { final WritableByteChannel writableByteChannel = (WritableByteChannel) this.selectionKey.channel(); if (this.currentEnvelope == null) { synchronized (this.queuedEnvelopes) { if (this.queuedEnvelopes.isEmpty()) { return false; } else { this.currentEnvelope = this.queuedEnvelopes.peek(); this.serializer.setTransferEnvelope(this.currentEnvelope); } } } if (!this.serializer.write(writableByteChannel)) { // Make sure we recycle the attached memory or file buffers correctly if (this.currentEnvelope.getBuffer() != null) { this.currentEnvelope.getBuffer().recycleBuffer(); } synchronized (this.queuedEnvelopes) { this.queuedEnvelopes.poll(); this.currentEnvelope = null; } } return true; } /** * Requests to close the underlying TCP connection. The request is ignored if at least one {@link TransferEnvelope} * is queued. * <p> * This method should only be called by the {@link OutgoingConnectionThread} object. * * @throws IOException * thrown if an error occurs while closing the TCP connection */ public void requestClose() throws IOException { synchronized (this.queuedEnvelopes) { if (this.queuedEnvelopes.isEmpty()) { if (this.isSubscribedToWriteEvent) { this.connectionThread.unsubscribeFromWriteEvent(this.selectionKey); this.isSubscribedToWriteEvent = false; } } } } /** * Closes the underlying TCP connection if no more {@link TransferEnvelope} objects are in the transmission queue. * <p> * This method should only be called by the {@link OutgoingConnectionThread} object. * * @throws IOException */ public void closeConnection() throws IOException { synchronized (this.queuedEnvelopes) { if (!this.queuedEnvelopes.isEmpty()) { return; } if (this.selectionKey != null) { final SocketChannel socketChannel = (SocketChannel) this.selectionKey.channel(); socketChannel.close(); this.selectionKey.cancel(); this.selectionKey = null; } this.isConnected = false; this.isSubscribedToWriteEvent = false; } } /** * Returns the number of queued {@link TransferEnvelope} objects with the given source channel ID. * * @param sourceChannelID * the source channel ID to count the queued envelopes for * @return the number of queued transfer envelopes with the given source channel ID */ public int getNumberOfQueuedEnvelopesFromChannel(final ChannelID sourceChannelID) { synchronized (this.queuedEnvelopes) { int number = 0; final Iterator<TransferEnvelope> it = this.queuedEnvelopes.iterator(); while (it.hasNext()) { final TransferEnvelope te = it.next(); if (sourceChannelID.equals(te.getSource())) { number++; } } return number; } } /** * Removes all queued {@link TransferEnvelope} objects from the transmission which match the given source channel * ID. * * @param sourceChannelID * the source channel ID of the transfered transfer envelopes to be dropped */ public void dropAllQueuedEnvelopesFromChannel(final ChannelID sourceChannelID) { synchronized (this.queuedEnvelopes) { final Iterator<TransferEnvelope> it = this.queuedEnvelopes.iterator(); while (it.hasNext()) { final TransferEnvelope te = it.next(); if (sourceChannelID.equals(te.getSource())) { it.remove(); if (te.getBuffer() != null) { te.getBuffer().recycleBuffer(); } } } } } /** * Checks whether this outgoing connection object manages an active connection or can be removed by the * {@link ByteBufferedChannelManager} object. * <p> * This method should only be called by the byte buffered channel manager. * * @return <code>true</code> if this object is no longer manages an active connection and can be removed, * <code>false</code> otherwise. */ public boolean canBeRemoved() { synchronized (this.queuedEnvelopes) { if (this.isConnected) { return false; } if (this.currentEnvelope != null) { return false; } return this.queuedEnvelopes.isEmpty(); } } /** * Sets the selection key representing the interest set of the underlying TCP NIO connection. * * @param selectionKey * the selection of the underlying TCP connection */ public void setSelectionKey(SelectionKey selectionKey) { this.selectionKey = selectionKey; } /** * Returns the number of currently queued envelopes which contain a write buffer. * * @return the number of currently queued envelopes which contain a write buffer */ public int getNumberOfQueuedWriteBuffers() { int retVal = 0; synchronized (this.queuedEnvelopes) { final Iterator<TransferEnvelope> it = this.queuedEnvelopes.iterator(); while (it.hasNext()) { final TransferEnvelope envelope = it.next(); if (envelope.getBuffer() != null) { ++retVal; } } } return retVal; } }