JDK-6179351 : (so) SocketChannel.close() causes select() to throw when setSoLinger option set
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.nio
  • Affected Version: 5.0,6
  • Priority: P4
  • Status: Closed
  • Resolution: Won't Fix
  • OS: linux,windows_xp
  • CPU: x86
  • Submitted: 2004-10-14
  • Updated: 2018-04-06
  • Resolved: 2018-04-06
Description
FULL PRODUCT VERSION :
java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0-b64)
Java HotSpot(TM) Client VM (build 1.5.0-b64, mixed mode, sharing)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows XP [Version 5.1.2600]

A DESCRIPTION OF THE PROBLEM :
This bug is similar to the (fixed) bug 4854354, but affects the close operation instead of the write operation.

For reference: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4854354

I created a non-blocking socket and set the linger on close socket option to 1 second. If the socket channel is closed immediately following a large write, the next call to select() throws an IOException. Moreover, the SocketChannel IS NOT CLOSED and the reader blocks on read indefinitely.

My goal is to be able to write a large amount of data and not close the socket until all this data has been (presumably) transmitted. This is the purpose of the setSoLinger(  true, secs ). If the linger on close option is very short, of course, there is no guarantee that the data will be sent; however, this should not cause the select() method to throw and break the loop managing all other channels. It should also retry the close operation so that the receiver is notified by reading end of stream.



STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Set the linger on close socket option on a non-blocking SocketChannel and call close after a large write - the close succeeds, but the next call to select() throws an IOException On my system, the size of the write generally needs to exceed 25K.

I have included a program that always produces the exception on my system. This code has been written to reproduce the problem and is not intended to represent production coding techniques.






EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
I expect a call to SocketChannel.close() to close the non-blocking socket. If linger on close is enabled, it should not close the socket until the specified linger duration has elapsed. There should be no case that would cause select() to throw simply because a buffer has not yet been written; after the linger duration has elapsed, the socket should be closed regardless of how much unwritten data still remains buffered, and the receiving side should get end of stream (-1).

ACTUAL -
The select() method throws an IOException if the SocketChannel still has not completed writing. The SocketChannel is not closed and the receiving side blocks waiting for end of stream (-1).

ERROR MESSAGES/STACK TRACES THAT OCCUR :
Exception in thread "main" java.io.IOException: A non-blocking socket operation could not be completed immediately
	at sun.nio.ch.SocketDispatcher.close0(Native Method)
	at sun.nio.ch.SocketDispatcher.close(Unknown Source)
	at sun.nio.ch.SocketChannelImpl.kill(Unknown Source)
	at sun.nio.ch.WindowsSelectorImpl.implDereg(Unknown Source)
	at sun.nio.ch.SelectorImpl.processDeregisterQueue(Unknown Source)
	at sun.nio.ch.WindowsSelectorImpl.doSelect(Unknown Source)
	at sun.nio.ch.SelectorImpl.lockAndDoSelect(Unknown Source)
	at sun.nio.ch.SelectorImpl.select(Unknown Source)
	at sun.nio.ch.SelectorImpl.select(Unknown Source)
	at Problem.main(Problem.java:33)



REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class Problem
{
    public static void main( String[] args )
    throws IOException
    {
        Selector selector = Selector.open();
        InetSocketAddress address = new InetSocketAddress( InetAddress.getLocalHost(), 0 );

        ServerSocketChannel listenChannel = ServerSocketChannel.open();
        listenChannel.socket().bind( address );
        listenChannel.configureBlocking( false );
        listenChannel.register( selector, SelectionKey.OP_ACCEPT );

        SocketChannel connectChannel = SocketChannel.open();
        connectChannel.configureBlocking( false );
        connectChannel.connect( new InetSocketAddress( InetAddress.getLocalHost(), listenChannel.socket().getLocalPort() ) );
        connectChannel.register( selector, SelectionKey.OP_CONNECT );

        selectorLoop: while ( true )
        {
            System.out.println( "Loop" );
            try
            {
                selector.select();
            }
            catch (IOException ex )
            {
                // Here is the problem: the socket is never actually closed!
                ex.printStackTrace();
                continue;
            }
            for ( Iterator it = selector.selectedKeys().iterator() ; it.hasNext() ; )
            {
                SelectionKey key = (SelectionKey) it.next();
                it.remove();
                if ( key.isValid() && key.isConnectable() )
                {
                    SocketChannel sc = (SocketChannel) key.channel();
                    if ( sc.finishConnect() )
                    {
                        System.out.println( "Connected " + sc );
                        sc.configureBlocking( false );
                        // setSoLinger() IS INCOMPATIBLE WITH NIO
                        sc.socket().setSoLinger( true, 1 );
                        sc.register( selector, SelectionKey.OP_WRITE );
                    }
                }
                if ( key.isValid() && key.isAcceptable() )
                {
                    SocketChannel sc = ((ServerSocketChannel) key.channel()).accept();
                    System.out.println( "Accepted " + sc );
                    sc.configureBlocking( false );
                    sc.register( selector, SelectionKey.OP_READ );
                }
                if ( key.isValid() && key.isWritable() )
                {
                    SocketChannel sc = (SocketChannel) key.channel();
                    int num = sc.write( ByteBuffer.wrap( new byte[ 524288 ] ) );
                    System.out.println( "Wrote " + num + " to " + sc );
                    sc.register( selector, key.interestOps() & ~SelectionKey.OP_WRITE );
                    // Call close immediately after a large write
                    sc.close();
                }
                if ( key.isValid() && key.isReadable() )
                {
                    SocketChannel sc = (SocketChannel) key.channel();
                    while ( true )
                    {
                        int num = sc.read( ByteBuffer.wrap( new byte[ 16384 ] ) );
                        if ( num < 0 )
                        {
                            System.out.println( "Socket closed normally" );
                            break selectorLoop;
                        }
                        else if ( num == 0 )
                        {
                            break;
                        }
                            
                        System.out.println( "Read " + num + " from " + sc );
                    }
                }
            }
        }
    }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Do not use the setSoLinger() (linger on close) socket option with a non-blocking socket. Instead, implement a timeout at the application level before closing a socket. Unfortunately, this can cause unreasonable delays on the receiving side of the socket, since the socket is not closed as soon as the last bytes are transmitted, but instead only after a timeout has elapsed. Some protocols (HTTP 1.0 for example) rely on the end of stream to immediately follow the last byte of data.

It is not sufficient to trap the IOException and ignore it, because the close() operation did not actually complete.

Another workaround:
===================
Although the bug is related to select()throwing an exception, and needs to be fixed, there is a simple way to avoid the exception.

After completely writing a buffer of data to the non-blocking SocketChannel, if the program wishes to close the socket, it should maintain a flag that indicates that this is desired rather than calling close() immediately following a write(). Instead, the program should remain interested in OP_WRITE for the SocketChannel and wait for select() to once again return that SelectionKey with isWritable() = true as if there were more data to write. The close() method can then be called without select() throwing the exception the next time it is called.
###@###.### 10/14/04 19:34 GMT
###@###.### 10/21/04 20:21 GMT

Comments
EVALUATION Setting the linger interval on a non-blocking SocketChannel leads to implementation specific behavior and is not recommended. In the current API there is no way to guarantee that the connection will be closed if this option is enabled and the close method is invoked to close the connection with data present.
21-11-2006