package org.webpieces.nio.impl.cm.basic;
import java.io.IOException;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import org.webpieces.data.api.BufferPool;
import org.webpieces.nio.api.channels.Channel;
import org.webpieces.nio.api.channels.ChannelSession;
import org.webpieces.nio.api.exceptions.NioClosedChannelException;
import org.webpieces.nio.api.exceptions.NioException;
import org.webpieces.nio.api.handlers.DataListener;
import org.webpieces.nio.api.handlers.RecordingDataListener;
import org.webpieces.nio.impl.util.ChannelSessionImpl;
/**
* @author Dean Hiller
*/
public abstract class BasChannelImpl
extends RegisterableChannelImpl
implements Channel {
private static final Logger apiLog = LoggerFactory.getLogger(Channel.class);
private static final Logger log = LoggerFactory.getLogger(BasChannelImpl.class);
private ChannelSession session = new ChannelSessionImpl();
private long waitingBytesCounter = 0;
private ConcurrentLinkedQueue<WriteInfo> dataToBeWritten = new ConcurrentLinkedQueue<WriteInfo>();
private boolean isClosed = false;
private boolean doNotAllowWrites;
private int maxBytesWaitingSize = 500_000; //0.5 megabyte before telling client to backpressure the channel
private AtomicBoolean applyingBackpressure = new AtomicBoolean(false);
private boolean isRegisterdForReads;
private BufferPool pool;
private DataListener dataListener;
private Object writeLock = new Object();
private boolean inDelayedWriteMode;
private boolean isRecording;
protected SocketAddress isConnectingTo;
public BasChannelImpl(IdObject id, SelectorManager2 selMgr, BufferPool pool) {
super(id, selMgr);
this.pool = pool;
this.isRecording = false;
}
/* (non-Javadoc)
* @see biz.xsoftware.nio.RegisterableChannelImpl#getRealChannel()
*/
public abstract SelectableChannel getRealChannel();
/* (non-Javadoc)
* @see api.biz.xsoftware.nio.RegisterableChannel#isBlocking()
*/
public abstract boolean isBlocking();
public abstract int readImpl(ByteBuffer b);
protected abstract int writeImpl(ByteBuffer b);
@Override
public CompletableFuture<Channel> connect(SocketAddress addr, DataListener listener) {
this.dataListener = listener;
if(isRecording)
dataListener = new RecordingDataListener("singleThreaded-", listener);
CompletableFuture<Channel> future = connectImpl(addr);
return future.thenApply(c -> {
registerForReads(dataListener);
return c;
});
}
protected abstract CompletableFuture<Channel> connectImpl(SocketAddress addr);
private void unqueueAndFailWritesThenClose(CloseRunnable action) {
List<CompletableFuture<Channel>> promises;
synchronized(this) { //put here for emphasis that we are synchronizing here but not below
promises = failAllWritesInQueue();
}
//TODO: This should really be inlined now. It's a remnant of an old design since close didn't
//work well outside the selector thread previously
action.runDelayedAction();
//we used to do this to put the close on the selector thread but if writes are held up it won't work
//registerForWritesOrClose();
//notify clients outside the synchronization block!!!
for(CompletableFuture<Channel> promise : promises) {
log.info("WRITES outstanding while close was called, notifying client through his failure method of the exception");
//we only incur the cost of Throwable.fillInStackTrace() if we will use this exception
//(it's called in the Throwable constructor) so we don't do this on every close channel
NioClosedChannelException closeExc = new NioClosedChannelException("There are "+promises.size()
+" writes that are not complete yet(you called write but "
+ "they did not call success back to the client).");
promise.completeExceptionally(closeExc);
}
}
@Override
public CompletableFuture<Channel> write(ByteBuffer b) {
if(b.remaining() == 0)
throw new IllegalArgumentException("buffer has no data");
else if(!getSelectorManager().isRunning())
throw new IllegalStateException(this+"ChannelManager must be running and is stopped");
else if(isClosed)
throw new NioClosedChannelException(this+"Client cannot write after the client closed the socket");
else if(doNotAllowWrites)
throw new IllegalStateException("This channel is in a failed state. "
+ "failure functions were called so look for exceptions from them");
apiLog.trace(()->this+"Basic.write");
CompletableFuture<Channel> future = new CompletableFuture<Channel>();
boolean wroteAllData = writeSynchronized(b, future);
if(wroteAllData) {
//since we didn't switch and were not in this mode, complete the action outside sync block
pool.releaseBuffer(b);
future.complete(this);
log.trace(()->this+" wrote bytes on client thread");
} else {
log.trace(()->this+"sent write to queue");
}
return future;
}
private boolean writeSynchronized(ByteBuffer b, CompletableFuture<Channel> future) {
//I feel like there is a bit too much in this sync block BUT this is also an extremely complex problem and
//VERY VERY VERY easy to get wrong. The calls I don't really like in here are registerForWrites()
//and dataListener.applyBackPressure but both are pretty complex AND it is very important to not have
//race conditions between
//1. turning on and off write registration with the selector
//2. turning on and off back pressure with the client
//These operations need to get to the selector or client IN ORDER and not have race conditions to work
//The corresponding code to worry about is writeAll method which reads from the waitingWriters
synchronized (writeLock ) {
if(!inDelayedWriteMode) {
int totalToWriteOut = b.remaining();
int written = writeImpl(b);
if(written != totalToWriteOut) {
if(b.remaining() + written != totalToWriteOut)
throw new IllegalStateException("Something went wrong. b.remaining()="+b.remaining()+" written="+written+" total="+totalToWriteOut);
registerForWrites();
inDelayedWriteMode = true;
} else
return true;
}
WriteInfo holder = new WriteInfo(b, future);
dataToBeWritten.add(holder);
boolean needToApplyBackpressure = false;
waitingBytesCounter += b.remaining();
if(waitingBytesCounter > maxBytesWaitingSize) {
needToApplyBackpressure = true;
}
boolean changedValue = applyingBackpressure.compareAndSet(false, needToApplyBackpressure);
if(needToApplyBackpressure && changedValue) {
//we only fire when the value of applyingBackpressure changes
//Also, this is a real PITA since we must do this in the sync block and I don't like calling
//customers in a sync block though thankfully most won't use the single threaded channelmanager.
//The reason is that if this is outside sync block, just before executor.execute({Runnable with call to
//applyBackPressure}) is called (run from the writer thread that is), the channelmanager thread
//can then call releaseBackPressure and that can beat applyBackPressure up the stack(and has)
//This results in the client permanently enabling pressure since it is the last call....when
//we need releaseBackPressure to always be after apply.
dataListener.applyBackPressure(this);
}
}
return false;
}
//synchronized with writeAll as both try to go through every element in the queue
//while most of the time there will be no contention(only on the close do we hit this)
private synchronized List<CompletableFuture<Channel>> failAllWritesInQueue() {
doNotAllowWrites = true;
List<CompletableFuture<Channel>> copy = new ArrayList<>();
while(!dataToBeWritten.isEmpty()) {
WriteInfo runnable = dataToBeWritten.remove();
ByteBuffer buffer = runnable.getBuffer();
buffer.position(buffer.limit()); //mark buffer read before releasing it
pool.releaseBuffer(buffer);
copy.add(runnable.getPromise());
}
waitingBytesCounter = 0;
return copy;
}
private void registerForWrites() {
log.trace(()->this+"registering channel for write msg. size="+dataToBeWritten.size());
getSelectorManager().registerSelectableChannel(this, SelectionKey.OP_WRITE, null);
}
/**
* This method is reading from the queue and writing out to the socket buffers that
* did not get written out when client called write.
*
*/
void writeAll() {
List<CompletableFuture<Channel>> finishedPromises = new ArrayList<>();
synchronized(writeLock) {
if(dataToBeWritten.isEmpty())
throw new IllegalStateException("bug, I am not sure this is possible..it shouldn't be...look into");
while(!dataToBeWritten.isEmpty()) {
WriteInfo writer = dataToBeWritten.peek();
ByteBuffer buffer = writer.getBuffer();
int initialSize = buffer.remaining();
int wroteOut = this.writeImpl(buffer);
if(buffer.hasRemaining()) {
if(buffer.remaining() + wroteOut != initialSize)
throw new IllegalStateException("Something went wrong. b.remaining()="+buffer.remaining()+" written="+wroteOut+" total="+initialSize);
log.trace(()->this+"Did not write all data out");
int leftOverSize = buffer.remaining();
int writtenOut = initialSize - leftOverSize;
waitingBytesCounter -= writtenOut;
break;
}
//if it finished, remove the item from the queue. It
//does not need to be run again.
dataToBeWritten.poll();
//release bytebuffer back to pool
pool.releaseBuffer(writer.getBuffer());
waitingBytesCounter -= initialSize;
finishedPromises.add(writer.getPromise());
}
//we are only applying backpressure when queue is too large
boolean applyPressure = !dataToBeWritten.isEmpty();
boolean changedValue = applyingBackpressure.compareAndSet(true, applyPressure);
if(!applyPressure && changedValue) {
//we only fire when the value of applyingBackpressure changes
dataListener.releaseBackPressure(this);
}
//we are registered for writes with ANY size queue
if(dataToBeWritten.isEmpty() && inDelayedWriteMode) {
inDelayedWriteMode = false;
log.trace(()->this+"unregister writes");
Helper.unregisterSelectableChannel(this, SelectionKey.OP_WRITE);
}
}
//MAKE SURE to notify clients outside of synchronization block so no deadlocks with their locks
for(CompletableFuture<Channel> promise : finishedPromises) {
promise.complete(this);
}
}
public void bind(SocketAddress addr) {
if(!(addr instanceof InetSocketAddress))
throw new IllegalArgumentException(this+"Can only bind to InetSocketAddress addressses");
apiLog.trace(()->this+"Basic.bind called addr="+addr);
try {
bindImpl(addr);
} catch (IOException e) {
throw new NioException(e);
}
}
private void bindImpl(SocketAddress addr) throws IOException {
try {
bindImpl2(addr);
} catch(Error e) {
//NOTE: jdk was throwing Error instead of BindException. We fix
//this and throw BindException which is the logical choice!!
//We are crossing our fingers hoping there are not other SocketExceptions
//from things other than address already in use!!!
if(e.getCause() instanceof SocketException) {
BindException exc = new BindException(e.getMessage());
exc.initCause(e.getCause());
throw exc;
}
throw e;
}
}
/**
*
* @param addr
* @throws IOException
*/
protected abstract void bindImpl2(SocketAddress addr) throws IOException;
void registerForReads(DataListener l) {
this.dataListener = l;
registerForReads();
}
public CompletableFuture<Channel> registerForReads() {
if(dataListener == null)
throw new IllegalArgumentException(this+"listener cannot be null");
else if(!isConnecting() && !isConnected()) {
throw new IllegalStateException(this+"Must call one of the connect methods first(ie. connect THEN register for reads)");
} else if(isClosed())
throw new IllegalStateException("Channel is closed");
apiLog.trace(()->this+"Basic.registerForReads called");
try {
return getSelectorManager().registerChannelForRead(this, dataListener).thenApply(v -> {
isRegisterdForReads = true;
return this;
});
} catch (IOException e) {
throw new NioException(e);
} catch (InterruptedException e) {
throw new NioException(e);
}
}
public CompletableFuture<Channel> unregisterForReads() {
apiLog.trace(()->this+"Basic.unregisterForReads called");
try {
isRegisterdForReads = false;
return getSelectorManager().unregisterChannelForRead(this).thenApply(v -> this);
} catch (IOException e) {
throw new NioException(e);
} catch (InterruptedException e) {
throw new NioException(e);
}
}
protected void setConnecting(SocketAddress addr) {
this.isConnectingTo = addr;
}
protected boolean isConnecting() {
return this.isConnectingTo != null;
}
protected void setClosed(boolean b) {
isClosed = b;
}
@Override
public CompletableFuture<Channel> close() {
//To prevent the following exception, in the readImpl method, we
//check if the socket is already closed, and if it is we don't read
//and just return -1 to indicate socket closed.
CompletableFuture<Channel> future = new CompletableFuture<>();
try {
apiLog.trace(()->this+"Basic.close called");
if(!getRealChannel().isOpen()) {
future.complete(this);
return future;
}
setClosed(true);
CloseRunnable runnable = new CloseRunnable(this, future);
unqueueAndFailWritesThenClose(runnable);
} catch(Exception e) {
log.error(this+"Exception closing channel", e);
future.completeExceptionally(e);
}
return future;
}
public void closeOnSelectorThread() throws IOException {
setClosed(true);
closeImpl();
}
protected abstract void closeImpl() throws IOException;
public ChannelSession getSession() {
return session;
}
@Override
public void setMaxBytesWriteBackupSize(int maxQueueSize) {
this.maxBytesWaitingSize = maxQueueSize;
}
@Override
public int getMaxBytesBackupSize() {
return maxBytesWaitingSize;
}
public boolean isRegisteredForReads() {
return isRegisterdForReads;
}
}