// Copyright 2016 Twitter. All rights reserved. // // 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 com.twitter.heron.common.network; import java.nio.channels.ClosedChannelException; import java.nio.channels.SocketChannel; import java.time.Duration; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.logging.Logger; import com.twitter.heron.common.basics.ByteAmount; import com.twitter.heron.common.basics.ISelectHandler; import com.twitter.heron.common.basics.NIOLooper; /** * This file defines the ChannelHelper class. * Heron Client and Server use the ChannelHelper class for doing all the reads/writes. A ChannelHelper * is a a placeholder for all network and io related information about a socket descriptor. It takes * care of async read/write of packets. Typical ways of instantiating ChannelHelper are described * below in a client and server settings. * <p> * SERVER * After an accept event, the server creates a ChannelHelper object giving it a proper option * ISelectHandler selectHandler = new Server(); * NIOLoop looper = new NIOLooper(); * SocketChannel socketChannel = ServerSocketChannel.accept(); * ChannelHelper channelHelper = new ChannelHelper(looper, selectHandler, socketChannel, options); * After this point, you could use read()/write() to access socket * <p> * CLIENT * After a successful connect, the client creates a ChannelHelper object giving it a proper option * ISelectHandler selectHandler = new Client(); * NIOLoop looper = new NIOLooper(); * SocketChannel socketChannel = SocketChannel.open(); * socketChannel.connect(endpoint); * ChannelHelper channelHelper = new ChannelHelper(looper, selectHandler, socketChannel, options); * After this point, you could use read()/write() to access socket. * <p> * Notice: The SocketChannelHelper would work only when the socketChannel is connected and opened. * Higher level logic needs to guarantee SocketChannelHelper's methods are invoked within proper * SocketChannel state. */ public class SocketChannelHelper { private static final Logger LOG = Logger.getLogger(SocketChannelHelper.class.getName()); private final NIOLooper looper; private final ISelectHandler selectHandler; private final SocketChannel socketChannel; // The unbounded queue of outstanding packets that need to be sent // Carefully check the size of queue before offering packets into it // to avoid the unbounded-growth of queue private final Queue<OutgoingPacket> outgoingPacketsToWrite; // System Config related private final ByteAmount writeBatchSize; private final Duration writeBatchTime; private final ByteAmount readBatchSize; private final Duration readReadBatchTime; // Incompletely read next packet private IncomingPacket incomingPacket; private long totalPacketsRead; private long totalPacketsWritten; private long totalBytesRead; private long totalBytesWritten; public SocketChannelHelper(NIOLooper looper, ISelectHandler selectHandler, SocketChannel socketChannel, HeronSocketOptions options) { this.looper = looper; this.selectHandler = selectHandler; this.socketChannel = socketChannel; this.outgoingPacketsToWrite = new LinkedList<OutgoingPacket>(); this.incomingPacket = new IncomingPacket(); this.writeBatchSize = options.getNetworkWriteBatchSize(); this.writeBatchTime = options.getNetworkWriteBatchTime(); this.readBatchSize = options.getNetworkReadBatchSize(); this.readReadBatchTime = options.getNetworkReadBatchTime(); // We will register Read by default when the connection is established // However, we will register Write only when we have something to write since // in most cases the socket will be writable but we have nothing to write this.enableReading(); } public void clear() { outgoingPacketsToWrite.clear(); } // Add this packet to the list of packets to be sent. The packet in itself can be sent // later. Packet should not be touched after sendPack on it is called // return value: // true indicates that the packet has been successfully queued to be // sent. It does not indicate that the packet was sent successfully. // false indicates an error. The most likely error is improperly // formatted packet. public boolean sendPacket(OutgoingPacket outgoingPacket) { // TODO -- add format check outgoingPacketsToWrite.add(outgoingPacket); enableWriting(); return true; } // Read bytes stream from socket and convert them into a list of IncomingPacket // It would return an empty list if something bad happens public List<IncomingPacket> read() { // We record the start time to avoid spending too much time on readings long startOfCycle = System.nanoTime(); long bytesRead = 0; long nPacketsRead = 0; List<IncomingPacket> ret = new ArrayList<IncomingPacket>(); // We would stop reading when: // 1. We spent too much time // 2. We have read large enough data while ((System.nanoTime() - startOfCycle - readReadBatchTime.toNanos()) < 0 && (bytesRead < readBatchSize.asBytes())) { int readState = incomingPacket.readFromChannel(socketChannel); if (readState > 0) { // Partial Read, just break, and read next time when the socket is readable break; } else if (readState < 0) { LOG.severe("Something bad happened while reading from channel: " + socketChannel.socket().getRemoteSocketAddress()); selectHandler.handleError(socketChannel); // Clear the list of Incoming Packet to avoid bad state is used externally ret.clear(); break; } else { // readState == 0, we fully read a incomingPacket nPacketsRead++; bytesRead += incomingPacket.size(); ret.add(incomingPacket); incomingPacket = new IncomingPacket(); } } totalPacketsRead += nPacketsRead; totalBytesRead += bytesRead; return ret; } // Write the outgoingPackets in buffer to socket public void write() { // We record the start time to avoid spending too much time on writings long startOfCycle = System.nanoTime(); long bytesWritten = 0; long nPacketsWritten = 0; while ((System.nanoTime() - startOfCycle - writeBatchTime.toNanos()) < 0 && (bytesWritten < writeBatchSize.asBytes())) { OutgoingPacket outgoingPacket = outgoingPacketsToWrite.peek(); if (outgoingPacket == null) { break; } int writeState = outgoingPacket.writeToChannel(socketChannel); if (writeState > 0) { // Partial writing, we would break since we could not write more data on socket. // But we have set the next start point of OutgoingPacket // Next time when the socket is writable, it will start from that point. break; } else if (writeState < 0) { LOG.severe("Something bad happened while writing to channel"); selectHandler.handleError(socketChannel); return; } else { // writeState == 0, we fully write a outgoingPacket bytesWritten += outgoingPacket.size(); nPacketsWritten++; outgoingPacketsToWrite.remove(); } } totalPacketsWritten += nPacketsWritten; totalBytesWritten += bytesWritten; // Disable writing if there are nothing to send in buffer if (getOutstandingPackets() == 0) { disableWriting(); } } // Force to flush all data in underneath buffer queue to socket with best effort // It is most likely happen when we are handling some unexpected cases, such as exiting public void forceFlushWithBestEffort() { LOG.info("Forcing to flush data to socket with best effort."); while (!outgoingPacketsToWrite.isEmpty()) { int writeState = outgoingPacketsToWrite.poll().writeToChannel(socketChannel); if (writeState != 0) { LOG.info("Failed to write more to Socket. Clear and finish the flush."); clear(); return; } } } public void enableReading() { if (!looper.isReadRegistered(socketChannel)) { try { looper.registerRead(socketChannel, selectHandler); } catch (ClosedChannelException e) { selectHandler.handleError(socketChannel); } } } public void disableReading() { if (looper.isReadRegistered(socketChannel)) { looper.unregisterRead(socketChannel); } } public void enableWriting() { if (!looper.isWriteRegistered(socketChannel)) { try { looper.registerWrite(socketChannel, selectHandler); } catch (ClosedChannelException e) { selectHandler.handleError(socketChannel); } } } public void disableWriting() { if (looper.isWriteRegistered(socketChannel)) { looper.unregisterWrite(socketChannel); } } public int getOutstandingPackets() { return outgoingPacketsToWrite.size(); } public boolean hasPacketsToSend() { return outgoingPacketsToWrite.size() > 0; } public long getTotalPacketsWritten() { return totalPacketsWritten; } public long getTotalPacketsRead() { return totalPacketsRead; } public long getTotalBytesRead() { return totalBytesRead; } public long getTotalBytesWritten() { return totalBytesWritten; } }