[jcifs] Thread leak problem on connect timeout

Data Shock datashock at hotmail.com
Thu Jun 25 19:35:17 GMT 2009


I am using the JCIFS library to write files to a Samba server.  In my implementation, the program checks a local directory for new files and writes them to the Samba server.  If the connect fails, it will try again after the next 30 seconds.

The problem is, I've found that if the server connect times out instead of a fast failure, threads are orphaned and steadily pile up.  The reason is that the connect itself doesn't timeout after 30 seconds, just the thread that's waiting for the the connect.  The connect itself doesn't time out for over 3 minutes.  The waiting thread simply orphans the connecting thread and moves on.  Additionally, the original connecting thread is still holding a lock, so all subsequent connect attempts must wait for the first to fail before they can continue.  This results in a steadily increasing number of threads.  Even though the original connecting thread will eventually timeout and exit, the waiting threads pile up faster.  This process will eventually exhaust the number of available system file descriptors and/or memory.

Perhaps it was coded this way for backwards Java version compatibility, but it would seem that a few things could be done to address the problem.

The simplest may be to interrupt the connecting thread then join with it.  However, I have read reports of Windows socket implementations not handling interrupts in blocking IO but I don't know if that problem still exists in more recent releases.

Another approach may be to update any TCP socket connect attempts to use:

Socket socket = new Socket();
socket.connect(socketaddress, timeout);

Instead of the simple constructor approach.  The connect with timeout has been available since Java 1.4.  I suppose this may be a Java compatibility problem, however I have not found any JCIFS documentation that specifies Java version compatibility.


Here is some code that can be used to reproduce the problem.  It requires one parameter:  A target host that will not reply to the Samba requests (to force the timeout).  It will create an SmbFile and call exists().  When that times out, it displays the names of all threads.  It repeats this 30 times to show the growing number of threads.  If the static boolean SHOW_STACK is set to true, a full stack trace will be shown instead of just thread names (so you can see where the threads are blocking).

* Note: For the generics and thread inspection to work, Java 5 is required.
** Further Note:  This code was formatted for a max of 80 chars width, which isn't the most readable but I thought it would be best for a mail list.





import java.net.MalformedURLException;

import jcifs.smb.SmbException;
import jcifs.smb.SmbFile;

/**
 * This class was created to show the flaw in the multi-threaded connect
 * approach.
 * 
 * The main method requires one argument: The name or address of a target that
 * will drop all SMB requests. It is important for the connect to timeout so
 * that the ever accumulating pile of threads can be shown. If the SMB request
 * is denied instead of dropped, this test will not work correctly.
 * 
 * The value of the static boolean SHOW_STACK determines if a stack trace for
 * each thread should be shown. If false, just the thread names are listed.
 */
public class TestSmbFileConnectTimeout {

    // Set this to true to add stack trace info to the thread inspection.
    private static final boolean SHOW_STACK = false;

    /**
     * @param args
     */
    public static void main(final String[] args) {
        if (args.length < 1) {
            System.err.println("No host specified");
            System.exit(1);
        }

        final String target = "smb://" + args[0] + "/foo";

        try {
            new TestSmbFileConnectTimeout().runTest(target);
        }
        catch (final MalformedURLException e) {
            System.err.println("Error with target URL [" + target + "]: "
                    + e.getLocalizedMessage());
            System.exit(2);
        }
    }

    private ThreadGroup getRootThreadGroup() {
        ThreadGroup rootThreadGroup = null;
        ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
        while (threadGroup != null) {
            rootThreadGroup = threadGroup;
            threadGroup = threadGroup.getParent();
        }

        if (rootThreadGroup == null) {
            System.err
                    .println("Unable to inspect threads (no root thread group)");
            System.exit(3);
        }

        return rootThreadGroup;
    }

    private void inspectThreads() {
        final StringBuilder threadInfo = new StringBuilder();

        final ThreadGroup threadGroup = getRootThreadGroup();
        // More than enough space for this test
        final Thread[] threads = new Thread[100];
        final int numThreads = threadGroup.enumerate(threads, true);
        if (numThreads> 0) {
            if (SHOW_STACK) {
                for (int i = 0; i < numThreads; i++) {
                    threadInfo.append(i + 1).append(' ').append(
                            threads[i].getName()).append(" state=").append(
                            threads[i].getState()).append('\n');
                    for (final StackTraceElement stElement : threads[i]
                            .getStackTrace()) {
                        threadInfo.append('\t').append(stElement.toString())
                                .append('\n');
                    }
                    threadInfo.append('\n');
                }
            }
            else {
                for (int i = 0; i < numThreads - 1; i++) {
                    threadInfo.append(threads[i].getName()).append(", ");
                }
                threadInfo.append(threads[numThreads - 1].getName());
            }
        }

        System.out.println("Threads[" + numThreads + "]: "
                + threadInfo.toString());
    }

    private void runTest(final String target) throws MalformedURLException {
        for (int i = 0; i < 30; i++) {
            final SmbFile smbFile = new SmbFile(target);
            System.out.println("SmbFile created: " + smbFile.toString());
            try {
                System.out.println("Checking exists (should force connect)...");
                System.out.println("Exists: " + smbFile.exists());
            }
            catch (final SmbException e) {
                System.out.println("Connect failed [" + e.getLocalizedMessage()
                        + "].  Inspecting threads...");
                inspectThreads();
            }
        }
    }
}



_________________________________________________________________
Windows Live™: Keep your life in sync. 
http://windowslive.com/explore?ocid=TXT_TAGLM_WL_BR_life_in_synch_062009


More information about the jcifs mailing list