package org.playorm.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.AsynchronousCloseException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import org.playorm.nio.api.channels.Channel; import org.playorm.nio.api.channels.NioException; import org.playorm.nio.api.handlers.DataListener; import org.playorm.nio.api.handlers.FutureOperation; import org.playorm.nio.api.handlers.OperationCallback; import org.playorm.nio.api.libs.BufferFactory; import org.playorm.nio.api.libs.ChannelSession; import org.playorm.nio.api.libs.FactoryCreator; import org.playorm.nio.impl.util.FutureOperationImpl; import org.playorm.nio.impl.util.UtilWaitForCompletion; /** * @author Dean Hiller */ public abstract class BasChannelImpl extends RegisterableChannelImpl implements Channel { private static final Logger apiLog = Logger.getLogger(Channel.class.getName()); private static final Logger log = Logger.getLogger(BasChannelImpl.class.getName()); private static final FactoryCreator CREATOR = FactoryCreator.createFactory(null); private ChannelSession session; private LinkedBlockingQueue<DelayedWritesCloses> waitingWriters = new LinkedBlockingQueue<DelayedWritesCloses>(1000); private boolean isConnecting = false; private boolean isClosed = false; private boolean registered; public BasChannelImpl(IdObject id, BufferFactory factory, SelectorManager2 selMgr) { super(id, selMgr); session = CREATOR.createSession(this); } /* (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) throws IOException; protected abstract int writeImpl(ByteBuffer b) throws IOException; /** * This is the method where writes are added to the queue to be written later when the selector * fires and tells me we have room to write again. * * @param id * @return true if the whole ByteBuffer was written, false if only part of it or none of it was written. * @throws IOException * @throws InterruptedException */ private synchronized void tryWriteOrClose(DelayedWritesCloses action) throws IOException, InterruptedException { //TODO: make 30 seconds configurable in milliseoncds maybe boolean accepted = waitingWriters.offer(action, 30, TimeUnit.SECONDS); if(!accepted) { throw new RuntimeException(this+"registered="+registered+" Dropping data, the upstream must be full as our queue is full of writes" + " that are stuck and can't go out(you should NOT call dataChunk.setProcessed in this case so the" + " downstream will slowdown and will not flood you as tcp flow control automatically kicks" + " in which means you will not flood the upstream like you are doing!!!!"); } //if not already registered, then register for writes..... //NOTE: we must do this after waitingWriters.offer so there is something on the queue to read //otherwise, that could be bad. if(!registered) { registered = true; if(log.isLoggable(Level.FINER)) log.finer(this+"registering channel for write msg cb="+action+" size="+waitingWriters.size()); getSelectorManager().registerSelectableChannel(this, SelectionKey.OP_WRITE, null, false); } } /** * This method is reading from the queue and writing out to the socket buffers that * did not get written out when client called write. * */ synchronized void writeAll() { Queue<DelayedWritesCloses> writers = waitingWriters; if(writers.isEmpty()) return; while(!writers.isEmpty()) { DelayedWritesCloses writer = writers.peek(); boolean finished = writer.runDelayedAction(true); if(!finished) { if(log.isLoggable(Level.FINER)) log.finer(this+"Did not write all of id="+writer); break; } //if it finished, remove the item from the queue. It //does not need to be run again. writers.remove(); } if(writers.isEmpty()) { if(log.isLoggable(Level.FINER)) log.fine(this+"unregister writes"); registered = false; Helper.unregisterSelectableChannel(this, SelectionKey.OP_WRITE); } } public void bind(SocketAddress addr) { if(!(addr instanceof InetSocketAddress)) throw new IllegalArgumentException(this+"Can only bind to InetSocketAddress addressses"); if(apiLog.isLoggable(Level.FINE)) apiLog.fine(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; public void registerForReads(DataListener listener) { if(listener == 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)"); } if(apiLog.isLoggable(Level.FINE)) apiLog.fine(this+"Basic.registerForReads called"); try { getSelectorManager().registerChannelForRead(this, listener); } catch (IOException e) { throw new NioException(e); } catch (InterruptedException e) { throw new NioException(e); } } public void unregisterForReads() { if(apiLog.isLoggable(Level.FINE)) apiLog.fine(this+"Basic.unregisterForReads called"); try { getSelectorManager().unregisterChannelForRead(this); } catch (IOException e) { throw new NioException(e); } catch (InterruptedException e) { throw new NioException(e); } } public int oldWrite(ByteBuffer b) { try { return oldWriteImpl(b); } catch (IOException e) { throw new NioException(e); } } private int oldWriteImpl(ByteBuffer b) throws IOException { if(!getSelectorManager().isRunning()) throw new IllegalStateException(this+"ChannelManager must be running and is stopped"); else if(isClosed) { AsynchronousCloseException exc = new AsynchronousCloseException(); IOException ioe = new IOException(this+"Client cannot write after the client closed the socket"); exc.initCause(ioe); throw exc; } Object t = getSelectorManager().getThread(); if(Thread.currentThread().equals(t)) { //leave this in, users should not do this.... throw new RuntimeException(this+"You should not perform a " + "blocking write on the channelmanager thread unless you like deadlock. " + "Use the cm threading layer, or put the code calling this write on another thread"); } try { int remain = b.remaining(); UtilWaitForCompletion waitWrite = new UtilWaitForCompletion(this, t); oldWrite(b, waitWrite); //otherwise if not all was written, wait for completion as it was added to queue //which writes on selector thread.... waitWrite.waitForComplete(); if(b.hasRemaining()) throw new RuntimeException(this+"Did not write all of the ByteBuffer out"); return remain; } catch(InterruptedException e) { throw new RuntimeException(e); } } @Override public FutureOperation write(ByteBuffer b) { try { return writeNewImpl(b); } catch (IOException e) { throw new NioException(e); } catch (InterruptedException e) { throw new NioException(e); } } private FutureOperation writeNewImpl(ByteBuffer b) throws IOException, InterruptedException { if(!getSelectorManager().isRunning()) throw new IllegalStateException(this+"ChannelManager must be running and is stopped"); else if(isClosed) { AsynchronousCloseException exc = new AsynchronousCloseException(); IOException ioe = new IOException(this+"Client cannot write after the client closed the socket"); exc.initCause(ioe); throw exc; } FutureOperationImpl impl = new FutureOperationImpl(); if(apiLog.isLoggable(Level.FINER)) apiLog.finer(this+"Basic.write"); //copy the buffer here ByteBuffer newOne = ByteBuffer.allocate(b.remaining()); newOne.put(b); newOne.flip(); WriteRunnable holder = new WriteRunnable(this, newOne, impl); tryWriteOrClose(holder); if(log.isLoggable(Level.FINER)) { log.finest(this+"sent write to queue"); } return impl; } public void oldWrite(ByteBuffer b, OperationCallback h) { try { oldWriteImpl(b, h); } catch (IOException e) { throw new NioException(e); } catch (InterruptedException e) { throw new NioException(e); } } public void oldWriteImpl(ByteBuffer b, OperationCallback h) throws IOException, InterruptedException { if(!getSelectorManager().isRunning()) throw new IllegalStateException(this+"ChannelManager must be running and is stopped"); else if(isClosed) { AsynchronousCloseException exc = new AsynchronousCloseException(); IOException ioe = new IOException(this+"Client cannot write after the client closed the socket"); exc.initCause(ioe); throw exc; } if(apiLog.isLoggable(Level.FINER)) apiLog.finer(this+"Basic.write callback="+h); //copy the buffer here ByteBuffer newOne = ByteBuffer.allocate(b.remaining()); newOne.put(b); newOne.flip(); WriteRunnable holder = new WriteRunnable(this, newOne, h); tryWriteOrClose(holder); if(log.isLoggable(Level.FINER)) { log.finest(this+"sent write to queue"); } } protected void setConnecting(boolean b) { isConnecting = b; } protected boolean isConnecting() { return isConnecting; } protected void setClosed(boolean b) { isClosed = b; } /* (non-Javadoc) * @see api.biz.xsoftware.nio.SocketChannel#close() */ public void oldClose() { Object t = getSelectorManager().getThread(); if(t != null && Thread.currentThread().equals(t)) { //leave this in, users should not do this.... throw new RuntimeException(this+"You should not perform a blocking close "+ "on the channelmanager thread for performance reasons. Use the cm threading layer, "+ "or put the code calling this write on another thread"); } try { UtilWaitForCompletion waitWrite = new UtilWaitForCompletion(this, null); oldClose(waitWrite); waitWrite.waitForComplete(); } catch(Exception e) { log.log(Level.WARNING, this+"Exception closing channel", e); } } public void oldClose(OperationCallback h) { //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. // //This is very complicated. It must be done after all the writes that have already //been called before the close was called. Basically, the close may need be //queued if there are writes on the queue. //unless you like to see the following //exception.......... //Feb 19, 2006 6:06:03 AM biz.xsoftware.test.nio.tcp.ZNioSuperclassTest verifyTearDown //INFO: CLIENT1 CLOSE //Feb 19, 2006 6:06:03 AM biz.xsoftware.impl.nio.cm.basic.Helper read //INFO: [[client]] Exception //java.nio.channels.ClosedChannelException // at sun.nio.ch.SocketChannelImpl.ensureReadOpen(SocketChannelImpl.java:112) // at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:139) // at biz.xsoftware.impl.nio.cm.basic.TCPChannelImpl.readImpl(TCPChannelImpl.java:162) // at biz.xsoftware.impl.nio.cm.basic.Helper.read(Helper.java:143) // at biz.xsoftware.impl.nio.cm.basic.Helper.processKey(Helper.java:92) // at biz.xsoftware.impl.nio.cm.basic.Helper.processKeys(Helper.java:47) // at biz.xsoftware.impl.nio.cm.basic.SelectorManager2.runLoop(SelectorManager2.java:305) // at biz.xsoftware.impl.nio.cm.basic.SelectorManager2$PollingThread.run(SelectorManager2.java:267) try { if(apiLog.isLoggable(Level.FINE)) apiLog.fine(this+"Basic.close called"); if(!getRealChannel().isOpen()) h.finished(this); setClosed(true); CloseRunnable runnable = new CloseRunnable(this, h); tryWriteOrClose(runnable); } catch(Exception e) { log.log(Level.WARNING, this+"Exception closing channel", e); } } @Override public FutureOperation 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. // //This is very complicated. It must be done after all the writes that have already //been called before the close was called. Basically, the close may need be //queued if there are writes on the queue. //unless you like to see the following //exception.......... //Feb 19, 2006 6:06:03 AM biz.xsoftware.test.nio.tcp.ZNioSuperclassTest verifyTearDown //INFO: CLIENT1 CLOSE //Feb 19, 2006 6:06:03 AM biz.xsoftware.impl.nio.cm.basic.Helper read //INFO: [[client]] Exception //java.nio.channels.ClosedChannelException // at sun.nio.ch.SocketChannelImpl.ensureReadOpen(SocketChannelImpl.java:112) // at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:139) // at biz.xsoftware.impl.nio.cm.basic.TCPChannelImpl.readImpl(TCPChannelImpl.java:162) // at biz.xsoftware.impl.nio.cm.basic.Helper.read(Helper.java:143) // at biz.xsoftware.impl.nio.cm.basic.Helper.processKey(Helper.java:92) // at biz.xsoftware.impl.nio.cm.basic.Helper.processKeys(Helper.java:47) // at biz.xsoftware.impl.nio.cm.basic.SelectorManager2.runLoop(SelectorManager2.java:305) // at biz.xsoftware.impl.nio.cm.basic.SelectorManager2$PollingThread.run(SelectorManager2.java:267) FutureOperationImpl future = new FutureOperationImpl(); try { if(apiLog.isLoggable(Level.FINE)) apiLog.fine(this+"Basic.close called"); if(!getRealChannel().isOpen()) future.finished(this); setClosed(true); CloseRunnable runnable = new CloseRunnable(this, future); tryWriteOrClose(runnable); } catch(Exception e) { log.log(Level.WARNING, this+"Exception closing channel", e); future.failed(this, e); } return future; } public void closeOnSelectorThread() throws IOException { setClosed(true); closeImpl(); } protected abstract void closeImpl() throws IOException; public ChannelSession getSession() { return session; } }