/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltcore.network;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import org.voltcore.logging.VoltLogger;
/** Encapsulates a socket registration for a VoltNetwork */
public class VoltPort implements Connection
{
/** The network this port participates in */
protected final VoltNetwork m_network;
protected static final VoltLogger networkLog = new VoltLogger("NETWORK");
public static final int MAX_MESSAGE_LENGTH = 52428800;
protected final NetworkDBBPool m_pool;
/** The currently selected operations on this port. */
private int m_readyOps = 0;
/** The operations the port wishes to have installed */
protected volatile int m_interestOps = 0;
/** True when the port is executing VoltNetwork dispatch */
volatile boolean m_running = false;
protected volatile boolean m_isDead = false;
protected volatile boolean m_isShuttingDown = false;
/** Used internally to make operation changes atomic.
* External writers (like a foreign host, for example),
* must be able to register network writes to a port
* that's running. This requires synchronization. */
protected final Object m_lock = new Object();
// BUG: should be final but currently VoltNetwork.register(), which
// generates the selection key, takes the port as a parameter. catch-22.
// Currently, only assigned in VoltNetwork.register(), which shouldn't
// be called on an already registered Port. As long as writes aren't
// queued to unregistered ports, this is thread safe.
protected SelectionKey m_selectionKey;
/** The channel this port wraps */
protected SocketChannel m_channel;
protected final InputHandler m_handler;
protected NIOReadStream m_readStream;
protected NIOWriteStream m_writeStream;
protected long m_messagesRead = 0;
private long m_lastMessagesRead = 0;
/*
* This variable will be changed to the actual hostname some time later. It
* is not guaranteed on how long it will take to do the reverse DNS lookup.
* It is not recommended to use the value of this variable as a key. Use
* m_remoteIP if you need to identify a host.
*/
volatile String m_remoteHostname = null;
final InetSocketAddress m_remoteSocketAddress;
final String m_remoteSocketAddressString;
private volatile String m_remoteHostAndAddressAndPort;
private String m_toString = null;
/** Wrap a socket with a VoltPort */
public VoltPort(
VoltNetwork network,
InputHandler handler,
InetSocketAddress remoteAddress,
NetworkDBBPool pool) {
m_network = network;
m_handler = handler;
m_remoteSocketAddress = remoteAddress;
m_remoteSocketAddressString = remoteAddress.getAddress().getHostAddress();
m_pool = pool;
m_remoteHostAndAddressAndPort = "/" + m_remoteSocketAddressString + ":" + m_remoteSocketAddress.getPort();
m_toString = super.toString() + ":" + m_remoteHostAndAddressAndPort;
}
/**
* Do a reverse DNS lookup of the remote end. Done in a separate thread unless synchronous is specified.
* If asynchronous lookup is requested the task may be dropped and resolution may never occur
*/
void resolveHostname(boolean synchronous) {
Runnable r = new Runnable() {
@Override
public void run() {
String remoteHost = ReverseDNSCache.hostnameOrAddress(m_remoteSocketAddress.getAddress());
if (!remoteHost.equals(m_remoteSocketAddress.getAddress().getHostAddress())) {
m_remoteHostname = remoteHost;
m_remoteHostAndAddressAndPort = remoteHost + m_remoteHostAndAddressAndPort;
m_toString = VoltPort.this.toString() + ":" + m_remoteHostAndAddressAndPort;
}
}
};
if (synchronous) {
r.run();
} else {
/*
* Start the reverse DNS lookup in background because it might be
* very slow if the hostname is not specified in local /etc/hosts.
*/
try {
ReverseDNSCache.submit(r);
} catch (RejectedExecutionException e) {
networkLog.debug(
"Reverse DNS lookup for " + m_remoteSocketAddress + " rejected because the queue was full");
}
}
}
protected void setKey (SelectionKey key) {
m_selectionKey = key;
m_channel = (SocketChannel)key.channel();
m_readStream = new NIOReadStream();
m_writeStream = new NIOWriteStream(
this,
m_handler.offBackPressure(),
m_handler.onBackPressure(),
m_handler.writestreamMonitor());
m_interestOps = key.interestOps();
}
/**
* Lock the VoltPort for running by the VoltNetwork executor service. This prevents anything from sneaking in a messing with
* the selector set until the executor service has had a chance to handle all the I/O.
*/
void lockForHandlingWork() {
synchronized(m_lock) {
assert m_running == false;
m_running = true;
m_readyOps = 0;
m_readyOps = m_selectionKey.readyOps(); // runnable.run() doesn't accept parameters
}
}
public void run() throws IOException {
try {
/*
* Have the read stream fill from the network
*/
if (readyForRead()) {
final int maxRead = m_handler.getMaxRead();
if (maxRead > 0) {
int read = fillReadStream(maxRead);
if (read > 0) {
try {
ByteBuffer message;
while ((message = m_handler.retrieveNextMessage(readStream())) != null) {
m_handler.handleMessage(message, this);
m_messagesRead++;
}
} catch (VoltProtocolHandler.BadMessageLength e) {
networkLog.error("Bad message length exception", e);
throw e;
}
}
}
}
/*
* On readiness selection, optimistically assume that write will succeed,
* in the common case it will
*/
drainWriteStream();
} finally {
synchronized(m_lock) {
assert(m_running == true);
m_running = false;
}
}
}
protected int fillReadStream(int maxBytes) throws IOException {
if ( maxBytes == 0 || m_isShuttingDown)
return 0;
// read from network, copy data into read buffers, which from thread local memory pool
final int read = m_readStream.read(m_channel, maxBytes, m_pool);
if (read == -1) {
disableReadSelection();
if (m_channel.socket().isConnected()) {
try {
m_channel.socket().shutdownInput();
} catch (SocketException e) {
//Safe to ignore to these
}
}
m_isShuttingDown = true;
m_handler.stopping(this);
/*
* Allow the write queue to drain if possible
*/
enableWriteSelection();
}
return read;
}
protected final void drainWriteStream() throws IOException {
//Safe to do this with a separate embedded synchronization because no interest ops are modded
m_writeStream.serializeQueuedWrites(m_pool);
/*
* All interactions with write stream must be protected
* with a lock to ensure that interests ops are consistent with
* the state of writes queued to the stream. This prevent
* lost queued writes where the write is queued
* but the write interest op is not set.
*/
synchronized (m_writeStream) {
/*
* If there is something to write always give it a whirl.
*/
if (!m_writeStream.isEmpty())
{
m_writeStream.drainTo(m_channel);
}
// Write selection is turned on when output data in enqueued,
// turn it off when the queue becomes empty.
if (m_writeStream.isEmpty()) {
disableWriteSelection();
if (m_isShuttingDown) {
m_channel.close();
//m_handler.stopped(this);
unregistered();
}
}
}
}
protected void enableWriteSelection() {
setInterests(SelectionKey.OP_WRITE, 0);
}
protected void disableWriteSelection() {
setInterests(0, SelectionKey.OP_WRITE);
}
@Override
public void disableReadSelection() {
setInterests(0, SelectionKey.OP_READ);
}
@Override
public void enableReadSelection() {
setInterests( SelectionKey.OP_READ, 0);
}
/** Report the operations the network should next select */
int interestOps() {
return m_interestOps;
}
/** Change the desired interest key set */
public void setInterests(int opsToAdd, int opsToRemove) {
// must be done atomically with changes to m_running
synchronized(m_lock) {
int oldInterestOps = m_interestOps;
m_interestOps = (m_interestOps | opsToAdd) & (~opsToRemove);
if (oldInterestOps != m_interestOps && !m_running) {
/*
* If this is a write, optimistically assume the write
* will succeed and try it without using the selector
*/
m_network.addToChangeList(this, (opsToAdd & SelectionKey.OP_WRITE) != 0);
}
}
}
/** Return the nio selection key underlying this port. */
public SelectionKey getKey() {
return m_selectionKey;
}
/** Return the operations that were last selected by the network */
int readyOps() {
return m_readyOps;
}
boolean readyForRead() {
return (readyOps() & SelectionKey.OP_READ) != 0 && (m_interestOps & SelectionKey.OP_READ) != 0;
}
boolean isRunning() {
return m_running;
}
void die() {
m_isDead = true;
}
boolean isDead() {
return m_isDead;
}
@Override
public NIOReadStream readStream() {
assert(m_readStream != null);
return m_readStream;
}
@Override
public NIOWriteStream writeStream() {
assert(m_writeStream != null);
return m_writeStream;
}
/**
* Invoked when the InputHandler is registering but not active
*/
void registering() {
m_handler.starting(this);
}
/**
* Invoked before the first message is sent/received
*/
void registered() {
m_handler.started(this);
}
/**
* Called when the unregistration has been requested. Could be do
* to app code or due to the client hanging up or some other error
* Spin until the port is no longer running.
*/
void unregistering() {
m_handler.stopping(this);
}
private boolean m_alreadyStopped = false;
/**
* Called when unregistration is complete and the Connection can no
* longer be interacted with.
*
* Various error paths fall back to unregistering so it can happen multiple times and is really
* annoying. Suppress it here with a flag
*/
void unregistered() {
try {
if (!m_alreadyStopped) {
m_alreadyStopped = true;
try {
m_handler.stopped(this);
} finally {
try {
m_writeStream.shutdown();
} finally {
if (m_readStream != null) {
m_readStream.shutdown();
}
}
}
}
} finally {
networkLog.debug("Closing channel " + m_toString);
try {
m_channel.close();
} catch (IOException e) {
networkLog.warn(e);
}
}
}
@Override
public String toString() {
if (m_toString == null) {
return super.toString();
} else {
return m_toString;
}
}
long getMessagesRead(boolean interval) {
if (interval) {
final long messagesRead = m_messagesRead;
final long messagesReadThisTime = messagesRead - m_lastMessagesRead;
m_lastMessagesRead = messagesRead;
return messagesReadThisTime;
} else {
return m_messagesRead;
}
}
@Override
public String getHostnameOrIP() {
if (m_remoteHostname != null) {
return m_remoteHostname;
} else {
return m_remoteSocketAddressString;
}
}
@Override
public String getHostnameOrIP(long clientHandle) {
return getHostnameOrIP();
}
@Override
public String getHostnameAndIPAndPort() {
return m_remoteHostAndAddressAndPort;
}
@Override
public int getRemotePort() {
return m_remoteSocketAddress.getPort();
}
@Override
public InetSocketAddress getRemoteSocketAddress() {
return m_remoteSocketAddress;
}
@Override
public long connectionId() {
return m_handler.connectionId();
}
@Override
public long connectionId(long clientHandle) {
return connectionId();
}
@Override
public Future<?> unregister() {
return m_network.unregisterChannel(this);
}
@Override
public void queueTask(Runnable r) {
m_network.queueTask(r);
}
}