/*
* Copyright (C) 2015 SoftIndex LLC.
*
* 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 io.datakernel.eventloop;
import io.datakernel.bytebuf.ByteBuf;
import io.datakernel.bytebuf.ByteBufPool;
import io.datakernel.exception.AsyncTimeoutException;
import io.datakernel.jmx.EventStats;
import io.datakernel.jmx.JmxAttribute;
import io.datakernel.jmx.ValueStats;
import io.datakernel.net.SocketSettings;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.ArrayDeque;
import static io.datakernel.util.Preconditions.checkNotNull;
@SuppressWarnings({"WeakerAccess", "AssertWithSideEffects"})
public final class AsyncTcpSocketImpl implements AsyncTcpSocket, NioChannelEventHandler {
public static final int DEFAULT_READ_BUF_SIZE = 16 * 1024;
public static final int OP_POSTPONED = 1 << 7; // SelectionKey constant
private static final int MAX_MERGE_SIZE = 16 * 1024;
@SuppressWarnings("ThrowableInstanceNeverThrown")
public static final AsyncTimeoutException TIMEOUT_EXCEPTION = new AsyncTimeoutException("timed out");
public static final int NO_TIMEOUT = -1;
private final Eventloop eventloop;
private final SocketChannel channel;
private final ArrayDeque<ByteBuf> writeQueue = new ArrayDeque<>();
private boolean writeEndOfStream;
private EventHandler socketEventHandler;
private SelectionKey key;
private int ops = 0;
private long readTimestamp = 0L;
private long writeTimestamp = 0L;
private long readTimeout = NO_TIMEOUT;
private long writeTimeout = NO_TIMEOUT;
protected int readMaxSize = DEFAULT_READ_BUF_SIZE;
protected int writeMaxSize = MAX_MERGE_SIZE;
private ScheduledRunnable checkReadTimeout;
private ScheduledRunnable checkWriteTimeout;
public interface Inspector {
void onReadTimeout();
void onRead(ByteBuf buf);
void onReadEndOfStream();
void onReadError(IOException e);
void onWriteTimeout();
void onWrite(ByteBuf buf, int bytes);
void onWriteError(IOException e);
}
public static class JmxInspector implements Inspector {
private static final double SMOOTHING_WINDOW = ValueStats.SMOOTHING_WINDOW_1_MINUTE;
private final ValueStats reads = ValueStats.create(SMOOTHING_WINDOW);
private final EventStats readEndOfStreams = EventStats.create(SMOOTHING_WINDOW);
private final EventStats readErrors = EventStats.create(SMOOTHING_WINDOW);
private final EventStats readTimeouts = EventStats.create(SMOOTHING_WINDOW);
private final ValueStats writes = ValueStats.create(SMOOTHING_WINDOW);
private final EventStats writeErrors = EventStats.create(SMOOTHING_WINDOW);
private final EventStats writeTimeouts = EventStats.create(SMOOTHING_WINDOW);
private final EventStats writeOverloaded = EventStats.create(SMOOTHING_WINDOW);
@Override
public void onReadTimeout() {
readTimeouts.recordEvent();
}
@Override
public void onRead(ByteBuf buf) {
reads.recordValue(buf.readRemaining());
}
@Override
public void onReadEndOfStream() {
readEndOfStreams.recordEvent();
}
@Override
public void onReadError(IOException e) {
readErrors.recordEvent();
}
@Override
public void onWriteTimeout() {
writeTimeouts.recordEvent();
}
@Override
public void onWrite(ByteBuf buf, int bytes) {
writes.recordValue(bytes);
if (buf.readRemaining() != bytes)
writeOverloaded.recordEvent();
}
@Override
public void onWriteError(IOException e) {
writeErrors.recordEvent();
}
@JmxAttribute
public EventStats getReadTimeouts() {
return readTimeouts;
}
@JmxAttribute
public ValueStats getReads() {
return reads;
}
@JmxAttribute
public EventStats getReadEndOfStreams() {
return readEndOfStreams;
}
@JmxAttribute
public EventStats getReadErrors() {
return readErrors;
}
@JmxAttribute
public EventStats getWriteTimeouts() {
return writeTimeouts;
}
@JmxAttribute
public ValueStats getWrites() {
return writes;
}
@JmxAttribute
public EventStats getWriteErrors() {
return writeErrors;
}
@JmxAttribute
public EventStats getWriteOverloaded() {
return writeOverloaded;
}
}
private Inspector inspector;
private final Runnable writeRunnable = new Runnable() {
@Override
public void run() {
if (writeTimestamp == 0L || !isOpen())
return;
writeTimestamp = 0L;
try {
doWrite();
} catch (IOException e) {
closeWithError(e, true);
}
}
};
// region builders
public static AsyncTcpSocketImpl wrapChannel(Eventloop eventloop, SocketChannel socketChannel,
SocketSettings socketSettings) {
try {
socketSettings.applySettings(socketChannel);
} catch (IOException e) {
throw new AssertionError("Failed to apply socketSettings", e);
}
AsyncTcpSocketImpl asyncTcpSocket = new AsyncTcpSocketImpl(eventloop, socketChannel);
if (socketSettings.hasImplReadTimeout())
asyncTcpSocket.readTimeout = socketSettings.getImplReadTimeout();
if (socketSettings.hasImplWriteTimeout())
asyncTcpSocket.writeTimeout = socketSettings.getImplWriteTimeout();
if (socketSettings.hasImplReadSize())
asyncTcpSocket.readMaxSize = socketSettings.getImplReadSize();
if (socketSettings.hasImplWriteSize())
asyncTcpSocket.writeMaxSize = socketSettings.getImplWriteSize();
return asyncTcpSocket;
}
public static AsyncTcpSocketImpl wrapChannel(Eventloop eventloop, SocketChannel socketChannel) {
return new AsyncTcpSocketImpl(eventloop, socketChannel);
}
public AsyncTcpSocketImpl withInspector(Inspector inspector) {
this.inspector = inspector;
return this;
}
private AsyncTcpSocketImpl(Eventloop eventloop, SocketChannel socketChannel) {
this.eventloop = checkNotNull(eventloop);
this.channel = checkNotNull(socketChannel);
}
// endregion
@Override
public void setEventHandler(EventHandler eventHandler) {
this.socketEventHandler = eventHandler;
}
public final void register() {
socketEventHandler.onRegistered();
try {
key = channel.register(eventloop.ensureSelector(), ops, this);
} catch (final IOException e) {
eventloop.post(new Runnable() {
@Override
public void run() {
closeChannel();
socketEventHandler.onClosedWithError(e);
}
});
}
if ((this.ops & SelectionKey.OP_READ) != 0) {
onReadReady();
}
}
// timeouts management
void scheduleReadTimeOut() {
if (checkReadTimeout == null) {
checkReadTimeout = eventloop.scheduleBackground(
eventloop.currentTimeMillis() + readTimeout,
new Runnable() {
@Override
public void run() {
if (inspector != null) inspector.onReadTimeout();
checkReadTimeout = null;
closeWithError(TIMEOUT_EXCEPTION, false);
}
});
}
}
void scheduleWriteTimeOut() {
if (checkWriteTimeout == null) {
checkWriteTimeout = eventloop.scheduleBackground(
eventloop.currentTimeMillis() + writeTimeout,
new Runnable() {
@Override
public void run() {
if (inspector != null) inspector.onWriteTimeout();
checkWriteTimeout = null;
closeWithError(TIMEOUT_EXCEPTION, false);
}
});
}
}
// interests management
@SuppressWarnings("MagicConstant")
private void interests(int newOps) {
if (ops != newOps) {
ops = newOps;
if ((ops & OP_POSTPONED) == 0 && key != null) {
key.interestOps(ops);
}
}
}
private void readInterest(boolean readInterest) {
interests(readInterest ? (ops | SelectionKey.OP_READ) : (ops & ~SelectionKey.OP_READ));
}
private void writeInterest(boolean writeInterest) {
interests(writeInterest ? (ops | SelectionKey.OP_WRITE) : (ops & ~SelectionKey.OP_WRITE));
}
@Override
public void read() {
if (readTimeout != NO_TIMEOUT) {
scheduleReadTimeOut();
}
readInterest(true);
if (readTimestamp == 0L) {
readTimestamp = eventloop.currentTimeMillis();
assert readTimestamp != 0L;
}
}
@Override
public void onReadReady() {
readTimestamp = 0L;
int oldOps = ops;
ops = ops | OP_POSTPONED;
readInterest(false);
int bytesRead = doRead();
if (bytesRead != 0) {
int newOps = ops & ~OP_POSTPONED;
ops = oldOps;
interests(newOps);
} else {
ops = oldOps;
}
}
private int doRead() {
ByteBuf buf = ByteBufPool.allocate(readMaxSize);
ByteBuffer buffer = buf.toWriteByteBuffer();
int numRead;
try {
numRead = channel.read(buffer);
buf.ofWriteByteBuffer(buffer);
} catch (IOException e) {
buf.recycle();
if (inspector != null) inspector.onReadError(e);
closeWithError(e, false);
return -1;
}
if (numRead == 0) {
if (inspector != null) inspector.onRead(buf);
buf.recycle();
return numRead;
}
if (checkReadTimeout != null) {
checkReadTimeout.cancel();
checkReadTimeout = null;
}
if (numRead == -1) {
buf.recycle();
if (inspector != null) inspector.onReadEndOfStream();
socketEventHandler.onReadEndOfStream();
return numRead;
}
if (inspector != null) inspector.onRead(buf);
socketEventHandler.onRead(buf);
return numRead;
}
// write cycle
@Override
public void write(ByteBuf buf) {
assert eventloop.inEventloopThread();
if (writeTimeout != NO_TIMEOUT) {
scheduleWriteTimeOut();
}
writeQueue.add(buf);
postWriteRunnable();
}
@Override
public void writeEndOfStream() {
assert eventloop.inEventloopThread();
if (writeEndOfStream) return;
writeEndOfStream = true;
postWriteRunnable();
}
@Override
public void onWriteReady() {
writeTimestamp = 0L;
try {
doWrite();
} catch (IOException e) {
closeWithError(e, false);
}
}
private void doWrite() throws IOException {
while (true) {
ByteBuf bufToSend = writeQueue.poll();
if (bufToSend == null)
break;
while (true) {
ByteBuf nextBuf = writeQueue.peek();
if (nextBuf == null)
break;
int bytesToCopy = nextBuf.readRemaining(); // bytes to append to bufToSend
if (bufToSend.readPosition() + bufToSend.readRemaining() + bytesToCopy > bufToSend.array().length)
bytesToCopy += bufToSend.readRemaining(); // append will resize bufToSend
if (bytesToCopy < writeMaxSize) {
bufToSend = ByteBufPool.append(bufToSend, nextBuf);
writeQueue.poll();
} else {
break;
}
}
@SuppressWarnings("ConstantConditions")
ByteBuffer bufferToSend = bufToSend.toReadByteBuffer();
try {
channel.write(bufferToSend);
} catch (IOException e) {
if (inspector != null) inspector.onWriteError(e);
bufToSend.recycle();
throw e;
}
if (inspector != null)
inspector.onWrite(bufToSend, bufferToSend.position() - bufToSend.readPosition());
bufToSend.ofReadByteBuffer(bufferToSend);
if (bufToSend.canRead()) {
writeQueue.addFirst(bufToSend); // put the buf back to the queue, to send it the next time
break;
}
bufToSend.recycle();
}
if (writeQueue.isEmpty()) {
if (checkWriteTimeout != null) {
checkWriteTimeout.cancel();
checkWriteTimeout = null;
}
if (writeEndOfStream) {
channel.shutdownOutput();
}
writeInterest(false);
socketEventHandler.onWrite();
} else {
writeInterest(true);
}
}
// close methods
@Override
public void close() {
assert eventloop.inEventloopThread();
if (key == null) return;
closeChannel();
key = null;
for (ByteBuf buf : writeQueue) {
buf.recycle();
}
writeQueue.clear();
if (checkWriteTimeout != null) {
checkWriteTimeout.cancel();
checkWriteTimeout = null;
}
if (checkReadTimeout != null) {
checkReadTimeout.cancel();
checkReadTimeout = null;
}
}
private void closeChannel() {
if (channel == null) return;
try {
channel.close();
} catch (IOException e) {
}
}
private void closeWithError(final Exception e, boolean fireAsync) {
if (isOpen()) {
close();
if (fireAsync)
eventloop.post(new Runnable() {
@Override
public void run() {
socketEventHandler.onClosedWithError(e);
}
});
else {
socketEventHandler.onClosedWithError(e);
}
}
}
// miscellaneous
private void postWriteRunnable() {
if (writeTimestamp == 0L) {
writeTimestamp = eventloop.currentTimeMillis();
assert writeTimestamp != 0L;
eventloop.post(writeRunnable);
}
}
public boolean isOpen() {
return key != null;
}
@Override
public InetSocketAddress getRemoteSocketAddress() {
try {
return (InetSocketAddress) channel.getRemoteAddress();
} catch (IOException ignored) {
throw new AssertionError("I/O error occurs or channel closed");
}
}
public SocketChannel getSocketChannel() {
return channel;
}
@Override
public String toString() {
String keyOps;
try {
keyOps = (key == null ? "" : opsToString(key.interestOps()));
} catch (Exception e) {
keyOps = "Key throwed exception: " + e.toString();
}
return "AsyncTcpSocketImpl{" +
"channel=" + (channel == null ? "" : channel.toString()) +
", writeQueueSize=" + writeQueue.size() +
", writeEndOfStream=" + writeEndOfStream +
", key.ops=" + keyOps +
", ops=" + opsToString(ops) +
", writing=" + (writeTimestamp != 0L) +
'}';
}
private String opsToString(int ops) {
StringBuilder sb = new StringBuilder();
if ((ops & OP_POSTPONED) != 0)
sb.append("OP_POSTPONED ");
if ((ops & SelectionKey.OP_WRITE) != 0)
sb.append("OP_WRITE ");
if ((ops & SelectionKey.OP_READ) != 0)
sb.append("OP_READ ");
return sb.toString();
}
}