/*
* Copyright 2009 Red Hat, Inc.
* Red Hat licenses this file to you 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 org.jboss.netty.channel.socket.nio;
import static org.jboss.netty.channel.Channels.*;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.log4j.Logger;
import org.jboss.netty.channel.AbstractChannelSink;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelException;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelState;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.logging.InternalLogger;
import org.jboss.netty.logging.InternalLoggerFactory;
import org.jboss.netty.util.ThreadRenamingRunnable;
import org.jboss.netty.util.internal.IoWorkerRunnable;
import org.jboss.netty.util.internal.LinkedTransferQueue;
/**
*
* @author The Netty Project (netty-dev@lists.jboss.org)
* @author Trustin Lee (tlee@redhat.com)
*
* @version $Rev: 1685 $, $Date: 2009-08-28 03:15:49 -0400 (Fri, 28 Aug 2009) $
*
*/
class NioClientSocketPipelineSink extends AbstractChannelSink {
static final Logger logger = Logger.getLogger( NioClientSocketPipelineSink.class );
private static final AtomicInteger nextId = new AtomicInteger( );
final int id = nextId.incrementAndGet( );
final Executor bossExecutor;
private final Boss boss = new Boss( );
private final NioWorker[] workers;
private final AtomicInteger workerIndex = new AtomicInteger( );
NioClientSocketPipelineSink( Executor bossExecutor, Executor workerExecutor, int workerCount ) {
this.bossExecutor = bossExecutor;
workers = new NioWorker[workerCount];
for ( int i = 0; i < workers.length; i++ ) {
workers[i] = new NioWorker( id, i + 1, workerExecutor );
}
}
public void eventSunk( ChannelPipeline pipeline, ChannelEvent e ) throws Exception {
if ( e instanceof ChannelStateEvent ) {
ChannelStateEvent event = ( ChannelStateEvent ) e;
NioClientSocketChannel channel = ( NioClientSocketChannel ) event.getChannel( );
ChannelFuture future = event.getFuture( );
ChannelState state = event.getState( );
Object value = event.getValue( );
switch ( state ) {
case OPEN:
if ( Boolean.FALSE.equals( value ) ) {
NioWorker.close( channel, future );
}
break;
case BOUND:
if ( value != null ) {
bind( channel, future, ( SocketAddress ) value );
} else {
NioWorker.close( channel, future );
}
break;
case CONNECTED:
if ( value != null ) {
connect( channel, future, ( SocketAddress ) value );
} else {
NioWorker.close( channel, future );
}
break;
case INTEREST_OPS:
NioWorker.setInterestOps( channel, future, ( ( Integer ) value ).intValue( ) );
break;
}
} else if ( e instanceof MessageEvent ) {
MessageEvent event = ( MessageEvent ) e;
NioSocketChannel channel = ( NioSocketChannel ) event.getChannel( );
boolean offered = channel.writeBuffer.offer( event );
assert offered;
NioWorker.write( channel, true );
}
}
private void bind( NioClientSocketChannel channel, ChannelFuture future, SocketAddress localAddress ) {
try {
channel.socket.socket( ).bind( localAddress );
channel.boundManually = true;
future.setSuccess( );
fireChannelBound( channel, channel.getLocalAddress( ) );
} catch ( Throwable t ) {
future.setFailure( t );
fireExceptionCaught( channel, t );
}
}
private void connect( final NioClientSocketChannel channel, ChannelFuture future, SocketAddress remoteAddress ) {
try {
if ( channel.socket.connect( remoteAddress ) ) {
channel.worker.register( channel, future );
} else {
future.addListener( ChannelFutureListener.CLOSE_ON_FAILURE );
channel.connectFuture = future;
boss.register( channel );
}
} catch ( Throwable t ) {
future.setFailure( t );
fireExceptionCaught( channel, t );
}
}
NioWorker nextWorker( ) {
return workers[Math.abs( workerIndex.getAndIncrement( ) % workers.length )];
}
private final class Boss implements Runnable {
volatile Selector selector;
private boolean started;
private final AtomicBoolean wakenUp = new AtomicBoolean( );
private final Object startStopLock = new Object( );
private final Queue<Runnable> registerTaskQueue = new LinkedTransferQueue<Runnable>( );
Boss( ) {
super( );
}
void register( NioClientSocketChannel channel ) {
Runnable registerTask = new RegisterTask( this, channel );
Selector selector;
synchronized ( startStopLock ) {
if ( !started ) {
// Open a selector if this worker didn't start yet.
try {
this.selector = selector = Selector.open( );
} catch ( Throwable t ) {
throw new ChannelException( "Failed to create a selector.", t );
}
// Start the worker thread with the new Selector.
boolean success = false;
try {
bossExecutor.execute( new IoWorkerRunnable( new ThreadRenamingRunnable( this, "New I/O client boss #" + id ) ) );
success = true;
} finally {
if ( !success ) {
// Release the Selector if the execution fails.
try {
selector.close( );
} catch ( Throwable t ) {
logger.warn( "Failed to close a selector.", t );
}
this.selector = selector = null;
// The method will return to the caller at this point.
}
}
} else {
// Use the existing selector if this worker has been started.
selector = this.selector;
}
assert selector != null && selector.isOpen( );
started = true;
boolean offered = registerTaskQueue.offer( registerTask );
assert offered;
}
if ( wakenUp.compareAndSet( false, true ) ) {
selector.wakeup( );
}
}
public void run( ) {
boolean shutdown = false;
Selector selector = this.selector;
long lastConnectTimeoutCheckTimeNanos = System.nanoTime( );
for ( ;; ) {
wakenUp.set( false );
try {
int selectedKeyCount = selector.select( 500 );
// 'wakenUp.compareAndSet(false, true)' is always evaluated
// before calling 'selector.wakeup()' to reduce the wake-up
// overhead. (Selector.wakeup() is an expensive operation.)
//
// However, there is a race condition in this approach.
// The race condition is triggered when 'wakenUp' is set to
// true too early.
//
// 'wakenUp' is set to true too early if:
// 1) Selector is waken up between 'wakenUp.set(false)' and
// 'selector.select(...)'. (BAD)
// 2) Selector is waken up between 'selector.select(...)' and
// 'if (wakenUp.get()) { ... }'. (OK)
//
// In the first case, 'wakenUp' is set to true and the
// following 'selector.select(...)' will wake up immediately.
// Until 'wakenUp' is set to false again in the next round,
// 'wakenUp.compareAndSet(false, true)' will fail, and therefore
// any attempt to wake up the Selector will fail, too, causing
// the following 'selector.select(...)' call to block
// unnecessarily.
//
// To fix this problem, we wake up the selector again if wakenUp
// is true immediately after selector.select(...).
// It is inefficient in that it wakes up the selector for both
// the first case (BAD - wake-up required) and the second case
// (OK - no wake-up required).
if ( wakenUp.get( ) ) {
selector.wakeup( );
}
processRegisterTaskQueue( );
if ( selectedKeyCount > 0 ) {
processSelectedKeys( selector.selectedKeys( ) );
}
// Handle connection timeout every 0.5 seconds approximately.
long currentTimeNanos = System.nanoTime( );
if ( currentTimeNanos - lastConnectTimeoutCheckTimeNanos >= 500 * 1000000L ) {
lastConnectTimeoutCheckTimeNanos = currentTimeNanos;
processConnectTimeout( selector.keys( ), currentTimeNanos );
}
// Exit the loop when there's nothing to handle.
// The shutdown flag is used to delay the shutdown of this
// loop to avoid excessive Selector creation when
// connection attempts are made in a one-by-one manner
// instead of concurrent manner.
if ( selector.keys( ).isEmpty( ) ) {
if ( shutdown || bossExecutor instanceof ExecutorService && ( ( ExecutorService ) bossExecutor ).isShutdown( ) ) {
synchronized ( startStopLock ) {
if ( registerTaskQueue.isEmpty( ) && selector.keys( ).isEmpty( ) ) {
started = false;
try {
selector.close( );
} catch ( IOException e ) {
logger.warn( "Failed to close a selector.", e );
} finally {
this.selector = null;
}
break;
} else {
shutdown = false;
}
}
} else {
// Give one more second.
shutdown = true;
}
} else {
shutdown = false;
}
} catch ( Throwable t ) {
logger.warn( "Unexpected exception in the selector loop.", t );
// Prevent possible consecutive immediate failures.
try {
Thread.sleep( 1000 );
} catch ( InterruptedException e ) {
// Ignore.
}
}
}
}
private void processRegisterTaskQueue( ) {
for ( ;; ) {
final Runnable task = registerTaskQueue.poll( );
if ( task == null ) {
break;
}
task.run( );
}
}
private void processSelectedKeys( Set<SelectionKey> selectedKeys ) {
for ( Iterator<SelectionKey> i = selectedKeys.iterator( ); i.hasNext( ); ) {
SelectionKey k = i.next( );
i.remove( );
if ( !k.isValid( ) ) {
close( k );
continue;
}
if ( k.isConnectable( ) ) {
connect( k );
}
}
}
private void processConnectTimeout( Set<SelectionKey> keys, long currentTimeNanos ) {
ConnectException cause = null;
for ( SelectionKey k : keys ) {
if ( !k.isValid( ) ) {
continue;
}
NioClientSocketChannel ch = ( NioClientSocketChannel ) k.attachment( );
if ( ch.connectDeadlineNanos > 0 && currentTimeNanos >= ch.connectDeadlineNanos ) {
if ( cause == null ) {
cause = new ConnectException( "connection timed out" );
}
ch.connectFuture.setFailure( cause );
fireExceptionCaught( ch, cause );
close( k );
}
}
}
private void connect( SelectionKey k ) {
NioClientSocketChannel ch = ( NioClientSocketChannel ) k.attachment( );
try {
if ( ch.socket.finishConnect( ) ) {
k.cancel( );
ch.worker.register( ch, ch.connectFuture );
}
} catch ( Throwable t ) {
ch.connectFuture.setFailure( t );
fireExceptionCaught( ch, t );
close( k );
}
}
private void close( SelectionKey k ) {
k.cancel( );
NioSocketChannel ch = ( NioSocketChannel ) k.attachment( );
NioWorker.close( ch, succeededFuture( ch ) );
}
}
private static final class RegisterTask implements Runnable {
private final Boss boss;
private final NioClientSocketChannel channel;
RegisterTask( Boss boss, NioClientSocketChannel channel ) {
this.boss = boss;
this.channel = channel;
}
public void run( ) {
try {
channel.socket.register( boss.selector, SelectionKey.OP_CONNECT, channel );
} catch ( ClosedChannelException e ) {
NioWorker.close( channel, succeededFuture( channel ) );
}
int connectTimeout = 5000;
if ( connectTimeout > 0 ) {
channel.connectDeadlineNanos = System.nanoTime( ) + connectTimeout * 1000000L;
}
}
}
}