From ed08a89377a333c10202ead88d355e16bcb3a0fd Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 27 Feb 2014 23:31:10 +0000
Subject: [PATCH] Backport fix for OPENDJ-1197: API is lacking functionality to specify TCP connect timeout

---
 opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/LDAPListenerOptions.java           |   94 +---
 opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/GrizzlyUtils.java                  |   74 +++
 opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties               |    2 
 opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/TimeoutChecker.java                |  100 +++--
 opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java             |  285 +++++++++++++++
 opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPServerFilter.java              |    8 
 opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPListenerImpl.java              |   16 
 opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPConnectionFactoryImpl.java     |   61 ++-
 opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/AsynchronousFutureResult.java      |   87 +++-
 opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPConnection.java                |   13 
 opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/LDAPOptions.java                   |  297 ++++++++-------
 opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPConnectionFactoryTestCase.java |   33 +
 opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/TimeoutEventListener.java          |   56 ++
 opendj-ldap-sdk/src/test/java/com/forgerock/opendj/ldap/LDAPConnectionTestCase.java        |    2 
 14 files changed, 814 insertions(+), 314 deletions(-)

diff --git a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/GrizzlyUtils.java b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/GrizzlyUtils.java
new file mode 100644
index 0000000..b9002d0
--- /dev/null
+++ b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/GrizzlyUtils.java
@@ -0,0 +1,74 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2013-2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap;
+
+import static com.forgerock.opendj.util.StaticUtils.DEBUG_LOG;
+
+import java.io.IOException;
+import java.net.SocketOption;
+import java.net.StandardSocketOptions;
+import java.nio.channels.SocketChannel;
+import java.util.logging.Level;
+
+import org.glassfish.grizzly.Connection;
+import org.glassfish.grizzly.nio.transport.TCPNIOConnection;
+
+/**
+ * Common utility methods.
+ */
+final class GrizzlyUtils {
+    static void configureConnection(final Connection<?> connection, final boolean tcpNoDelay,
+            final boolean keepAlive, final boolean reuseAddress, final int linger) {
+        /*
+         * Test shows that its much faster with non block writes but risk
+         * running out of memory if the server is slow.
+         */
+        connection.configureBlocking(true);
+
+        // Configure socket options.
+        final SocketChannel channel = (SocketChannel) ((TCPNIOConnection) connection).getChannel();
+        setSocketOption(channel, StandardSocketOptions.TCP_NODELAY, tcpNoDelay);
+        setSocketOption(channel, StandardSocketOptions.SO_KEEPALIVE, keepAlive);
+        setSocketOption(channel, StandardSocketOptions.SO_REUSEADDR, reuseAddress);
+        setSocketOption(channel, StandardSocketOptions.SO_LINGER, linger);
+    }
+
+    private static <T> void setSocketOption(final SocketChannel channel,
+            final SocketOption<T> option, final T value) {
+        try {
+            channel.setOption(option, value);
+        } catch (final IOException e) {
+            DEBUG_LOG.log(Level.FINE, "Unable to set " + option.name() + " to " + value
+                    + " on client connection", e);
+        }
+    }
+
+    // Prevent instantiation.
+    private GrizzlyUtils() {
+        // No implementation required.
+    }
+
+}
diff --git a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPConnection.java b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPConnection.java
index ed4bd3f..1c16c06 100644
--- a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPConnection.java
+++ b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPConnection.java
@@ -87,7 +87,8 @@
 /**
  * LDAP connection implementation.
  */
-final class LDAPConnection extends AbstractAsynchronousConnection implements Connection {
+final class LDAPConnection extends AbstractAsynchronousConnection implements Connection,
+        TimeoutEventListener {
     /**
      * A dummy SSL client engine configurator as SSLFilter only needs client
      * config. This prevents Grizzly from needlessly using JVM defaults which
@@ -385,6 +386,11 @@
     }
 
     @Override
+    public long getTimeout() {
+        return factory.getLDAPOptions().getTimeout(TimeUnit.MILLISECONDS);
+    }
+
+    @Override
     public <R extends ExtendedResult> FutureResult<R> extendedRequestAsync(
             final ExtendedRequest<R> request,
             final IntermediateResponseHandler intermediateResponseHandler,
@@ -568,7 +574,8 @@
         return builder.toString();
     }
 
-    long cancelExpiredRequests(final long currentTime) {
+    @Override
+    public long handleTimeout(final long currentTime) {
         final long timeout = factory.getLDAPOptions().getTimeout(TimeUnit.MILLISECONDS);
         if (timeout <= 0) {
             return 0;
@@ -701,7 +708,7 @@
             } finally {
                 asn1Writer.recycle();
             }
-            factory.getTimeoutChecker().removeConnection(this);
+            factory.getTimeoutChecker().removeListener(this);
             connection.closeSilently();
             factory.releaseTransportAndTimeoutChecker();
         }
diff --git a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPConnectionFactoryImpl.java b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPConnectionFactoryImpl.java
index 49d6fa1..2f397d2 100644
--- a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPConnectionFactoryImpl.java
+++ b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPConnectionFactoryImpl.java
@@ -28,7 +28,9 @@
 package com.forgerock.opendj.ldap;
 
 import static com.forgerock.opendj.ldap.DefaultTCPNIOTransport.DEFAULT_TRANSPORT;
+import static com.forgerock.opendj.ldap.GrizzlyUtils.configureConnection;
 import static com.forgerock.opendj.ldap.TimeoutChecker.TIMEOUT_CHECKER;
+import static org.forgerock.opendj.ldap.CoreMessages.LDAP_CONNECTION_CONNECT_TIMEOUT;
 import static org.forgerock.opendj.ldap.ErrorResultException.*;
 
 import java.io.IOException;
@@ -72,13 +74,16 @@
      */
     @SuppressWarnings("rawtypes")
     private final class CompletionHandlerAdapter implements
-            CompletionHandler<org.glassfish.grizzly.Connection> {
-
+            CompletionHandler<org.glassfish.grizzly.Connection>, TimeoutEventListener {
         private final AsynchronousFutureResult<Connection, ResultHandler<? super Connection>> future;
+        private final long timeoutEndTime;
 
         private CompletionHandlerAdapter(
                 final AsynchronousFutureResult<Connection, ResultHandler<? super Connection>> future) {
             this.future = future;
+            final long timeoutMS = getTimeout();
+            this.timeoutEndTime = timeoutMS > 0 ? System.currentTimeMillis() + timeoutMS : 0;
+            timeoutChecker.get().addListener(this);
         }
 
         @Override
@@ -99,10 +104,10 @@
 
             // Start TLS or install SSL layer asynchronously.
 
-            // Give up immediately if the future has been cancelled.
-            if (future.isCancelled()) {
+            // Give up immediately if the future has been cancelled or timed out.
+            if (future.isDone()) {
+                timeoutChecker.get().removeListener(this);
                 connection.close();
-                releaseTransportAndTimeoutChecker();
                 return;
             }
 
@@ -151,6 +156,7 @@
         @Override
         public void failed(final Throwable throwable) {
             // Adapt and forward.
+            timeoutChecker.get().removeListener(this);
             future.handleErrorResult(adaptConnectionException(throwable));
             releaseTransportAndTimeoutChecker();
         }
@@ -161,16 +167,11 @@
         }
 
         private LDAPConnection adaptConnection(final org.glassfish.grizzly.Connection<?> connection) {
-            /*
-             * Test shows that its much faster with non block writes but risk
-             * running out of memory if the server is slow.
-             */
-            connection.configureBlocking(true);
+            configureConnection(connection, options.isTCPNoDelay(), options.isKeepAlive(), options
+                    .isReuseAddress(), options.getLinger());
             final LDAPConnection ldapConnection =
                     new LDAPConnection(connection, LDAPConnectionFactoryImpl.this);
-            if (options.getTimeout(TimeUnit.MILLISECONDS) > 0) {
-                timeoutChecker.get().addConnection(ldapConnection);
-            }
+            timeoutChecker.get().addListener(ldapConnection);
             clientFilter.registerConnection(connection, ldapConnection);
             return ldapConnection;
         }
@@ -189,20 +190,37 @@
 
         private void onFailure(final LDAPConnection connection, final Throwable t) {
             // Abort connection attempt due to error.
-            connection.close();
+            timeoutChecker.get().removeListener(this);
             future.handleErrorResult(adaptConnectionException(t));
-            releaseTransportAndTimeoutChecker();
+            connection.close();
         }
 
         private void onSuccess(final LDAPConnection connection) {
-            future.handleResult(connection);
-
-            // Close the connection if the future was cancelled.
-            if (future.isCancelled()) {
+            timeoutChecker.get().removeListener(this);
+            if (!future.tryHandleResult(connection)) {
+                // The connection has been either cancelled or it has timed out.
                 connection.close();
-                releaseTransportAndTimeoutChecker();
             }
         }
+
+        @Override
+        public long handleTimeout(final long currentTime) {
+            if (timeoutEndTime == 0) {
+                return 0;
+            } else if (timeoutEndTime > currentTime) {
+                return timeoutEndTime - currentTime;
+            } else {
+                future.handleErrorResult(newErrorResult(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
+                        LDAP_CONNECTION_CONNECT_TIMEOUT.get(socketAddress.toString(), getTimeout())
+                                .toString()));
+                return 0;
+            }
+        }
+
+        @Override
+        public long getTimeout() {
+            return options.getConnectTimeout(TimeUnit.MILLISECONDS);
+        }
     }
 
     private final LDAPClientFilter clientFilter;
@@ -271,8 +289,7 @@
                         .build();
         final AsynchronousFutureResult<Connection, ResultHandler<? super Connection>> future =
                 new AsynchronousFutureResult<Connection, ResultHandler<? super Connection>>(handler);
-        final CompletionHandlerAdapter cha = new CompletionHandlerAdapter(future);
-        connectorHandler.connect(socketAddress, cha);
+        connectorHandler.connect(socketAddress, new CompletionHandlerAdapter(future));
         return future;
     }
 
diff --git a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPListenerImpl.java b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPListenerImpl.java
index e1d9604..a91b1da 100644
--- a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPListenerImpl.java
+++ b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPListenerImpl.java
@@ -36,7 +36,6 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Level;
 
-import org.forgerock.opendj.ldap.DecodeOptions;
 import org.forgerock.opendj.ldap.LDAPClientContext;
 import org.forgerock.opendj.ldap.LDAPListenerOptions;
 import org.forgerock.opendj.ldap.ServerConnectionFactory;
@@ -60,6 +59,7 @@
     private final TCPNIOServerConnection serverConnection;
     private final AtomicBoolean isClosed = new AtomicBoolean();
     private final InetSocketAddress socketAddress;
+    private final LDAPListenerOptions options;
 
     /**
      * Creates a new LDAP listener implementation which will listen for LDAP
@@ -81,12 +81,12 @@
             final LDAPListenerOptions options) throws IOException {
         this.transport = DEFAULT_TRANSPORT.acquireIfNull(options.getTCPNIOTransport());
         this.connectionFactory = factory;
-
-        final DecodeOptions decodeOptions = new DecodeOptions(options.getDecodeOptions());
+        this.options = new LDAPListenerOptions(options);
+        final LDAPServerFilter serverFilter =
+                new LDAPServerFilter(this, new LDAPReader(this.options.getDecodeOptions()),
+                        this.options.getMaxRequestSize());
         this.defaultFilterChain =
-                FilterChainBuilder.stateless().add(new TransportFilter()).add(
-                        new LDAPServerFilter(this, new LDAPReader(decodeOptions), options
-                                .getMaxRequestSize())).build();
+                FilterChainBuilder.stateless().add(new TransportFilter()).add(serverFilter).build();
         final TCPNIOBindingHandler bindingHandler =
                 TCPNIOBindingHandler.builder(transport.get()).processor(defaultFilterChain).build();
         this.serverConnection = bindingHandler.bind(address, options.getBacklog());
@@ -140,4 +140,8 @@
     FilterChain getDefaultFilterChain() {
         return defaultFilterChain;
     }
+
+    LDAPListenerOptions getLDAPListenerOptions() {
+        return options;
+    }
 }
diff --git a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPServerFilter.java b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPServerFilter.java
index abd829a..7fec5b1 100644
--- a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPServerFilter.java
+++ b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/LDAPServerFilter.java
@@ -22,11 +22,12 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012-2013 ForgeRock AS.
+ *      Portions copyright 2012-2014 ForgeRock AS.
  */
 
 package com.forgerock.opendj.ldap;
 
+import static com.forgerock.opendj.ldap.GrizzlyUtils.configureConnection;
 import static com.forgerock.opendj.ldap.LDAPConstants.OID_NOTICE_OF_DISCONNECTION;
 
 import java.io.IOException;
@@ -43,6 +44,7 @@
 import org.forgerock.opendj.ldap.ErrorResultException;
 import org.forgerock.opendj.ldap.IntermediateResponseHandler;
 import org.forgerock.opendj.ldap.LDAPClientContext;
+import org.forgerock.opendj.ldap.LDAPListenerOptions;
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.ResultHandler;
 import org.forgerock.opendj.ldap.SearchResultHandler;
@@ -813,7 +815,9 @@
     @Override
     public NextAction handleAccept(final FilterChainContext ctx) throws IOException {
         final Connection<?> connection = ctx.getConnection();
-        connection.configureBlocking(true);
+        LDAPListenerOptions options = listener.getLDAPListenerOptions();
+        configureConnection(connection, options.isTCPNoDelay(), options.isKeepAlive(), options
+                .isReuseAddress(), options.getLinger());
         try {
             final ClientContextImpl clientContext = new ClientContextImpl(connection);
             final ServerConnection<Integer> serverConn =
diff --git a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/TimeoutChecker.java b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/TimeoutChecker.java
index b169be8..4631bbb 100644
--- a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/TimeoutChecker.java
+++ b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/TimeoutChecker.java
@@ -22,9 +22,8 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2013 ForgeRock AS.
+ *      Portions copyright 2013-2014 ForgeRock AS.
  */
-
 package com.forgerock.opendj.ldap;
 
 import static com.forgerock.opendj.util.StaticUtils.DEBUG_LOG;
@@ -37,13 +36,17 @@
 import com.forgerock.opendj.util.ReferenceCountedObject;
 
 /**
- * Checks connection for pending requests that have timed out.
+ * Checks {@code TimeoutEventListener listeners} for events that have timed out.
+ * <p>
+ * All listeners registered with the {@code #addListener()} method are called
+ * back with {@code TimeoutEventListener#handleTimeout()} to be able to handle
+ * the timeout.
  */
-final class TimeoutChecker {
+public final class TimeoutChecker {
     /**
-     * Global reference counted instance.
+     * Global reference on the timeout checker.
      */
-    static final ReferenceCountedObject<TimeoutChecker> TIMEOUT_CHECKER =
+    public static final ReferenceCountedObject<TimeoutChecker> TIMEOUT_CHECKER =
             new ReferenceCountedObject<TimeoutChecker>() {
                 @Override
                 protected void destroyInstance(final TimeoutChecker instance) {
@@ -62,11 +65,11 @@
     private final Object stateLock = new Object();
 
     /**
-     * The connection set must be safe from CMEs because expiring requests can
-     * cause the connection to be closed.
+     * The listener set must be safe from CMEs. For example, if the listener is
+     * a connection, expiring requests can cause the connection to be closed.
      */
-    private final Set<LDAPConnection> connections =
-            newSetFromMap(new ConcurrentHashMap<LDAPConnection, Boolean>());
+    private final Set<TimeoutEventListener> listeners =
+            newSetFromMap(new ConcurrentHashMap<TimeoutEventListener, Boolean>());
 
     /**
      * Used to signal thread shutdown.
@@ -74,48 +77,46 @@
     private volatile boolean shutdownRequested = false;
 
     /**
-     * Used for signalling that new connections have been added while performing
-     * timeout processing.
+     * Contains the minimum delay for listeners which were added while the
+     * timeout check was not sleeping (i.e. while it was processing listeners).
      */
-    private volatile boolean pendingNewConnections = false;
+    private volatile long pendingListenerMinDelay = Long.MAX_VALUE;
 
     private TimeoutChecker() {
-        final Thread checkerThread = new Thread("OpenDJ LDAP SDK Connection Timeout Checker") {
+        final Thread checkerThread = new Thread("OpenDJ LDAP SDK Timeout Checker") {
             @Override
             public void run() {
                 DEBUG_LOG.fine("Timeout Checker Starting");
                 while (!shutdownRequested) {
-                    final long currentTime = System.currentTimeMillis();
-                    long delay = 0;
                     /*
-                     * New connections may be added during iteration and may be
-                     * missed resulting in the timeout checker waiting longer
-                     * than it should, or even forever (e.g. if the new
-                     * connection is the first).
+                     * New listeners may be added during iteration and may not
+                     * be included in the computation of the new delay. This
+                     * could potentially result in the timeout checker waiting
+                     * longer than it should, or even forever (e.g. if the new
+                     * listener is the first).
                      */
-                    pendingNewConnections = false;
-                    for (final LDAPConnection connection : connections) {
+                    final long currentTime = System.currentTimeMillis();
+                    long delay = Long.MAX_VALUE;
+                    pendingListenerMinDelay = Long.MAX_VALUE;
+                    for (final TimeoutEventListener listener : listeners) {
                         if (DEBUG_LOG.isLoggable(Level.FINER)) {
-                            DEBUG_LOG.finer("Checking connection " + connection + " delay = "
+                            DEBUG_LOG.finer("Checking listener " + listener + " delay = "
                                     + delay);
                         }
-
                         // May update the connections set.
-                        final long newDelay = connection.cancelExpiredRequests(currentTime);
+                        final long newDelay = listener.handleTimeout(currentTime);
                         if (newDelay > 0) {
-                            if (delay > 0) {
-                                delay = Math.min(newDelay, delay);
-                            } else {
-                                delay = newDelay;
-                            }
+                            delay = Math.min(newDelay, delay);
                         }
                     }
 
                     try {
                         synchronized (stateLock) {
-                            if (shutdownRequested || pendingNewConnections) {
-                                // Loop immediately.
-                                pendingNewConnections = false;
+                            // Include any pending listener delays.
+                            delay = Math.min(pendingListenerMinDelay, delay);
+                            if (shutdownRequested) {
+                                // Stop immediately.
+                                break;
                             } else if (delay <= 0) {
                                 /*
                                  * If there is at least one connection then the
@@ -137,16 +138,35 @@
         checkerThread.start();
     }
 
-    void addConnection(final LDAPConnection connection) {
-        connections.add(connection);
-        synchronized (stateLock) {
-            pendingNewConnections = true;
-            stateLock.notifyAll();
+    /**
+     * Registers a timeout event listener for timeout notification.
+     *
+     * @param listener
+     *            The timeout event listener.
+     */
+    public void addListener(final TimeoutEventListener listener) {
+        /*
+         * Only add the listener if it has a non-zero timeout. This assumes that
+         * the timeout is fixed.
+         */
+        final long timeout = listener.getTimeout();
+        if (timeout > 0) {
+            listeners.add(listener);
+            synchronized (stateLock) {
+                pendingListenerMinDelay = Math.min(pendingListenerMinDelay, timeout);
+                stateLock.notifyAll();
+            }
         }
     }
 
-    void removeConnection(final LDAPConnection connection) {
-        connections.remove(connection);
+    /**
+     * Deregisters a timeout event listener for timeout notification.
+     *
+     * @param listener
+     *            The timeout event listener.
+     */
+    public void removeListener(final TimeoutEventListener listener) {
+        listeners.remove(listener);
         // No need to signal.
     }
 
diff --git a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/TimeoutEventListener.java b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/TimeoutEventListener.java
new file mode 100644
index 0000000..01bdd04
--- /dev/null
+++ b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/ldap/TimeoutEventListener.java
@@ -0,0 +1,56 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2013-2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap;
+
+/**
+ * Listener on timeout events.
+ * <p>
+ * The listener must register itself with a {@code TimeoutChecker} using
+ * {@code TimeoutChecker#addListener()} method to be called back with
+ * {@code #handleTimeout} method.
+ * <p>
+ * The listener must deregister itself using
+ * {@code TimeoutChecker#removeListener()} to stop being called back.
+ */
+public interface TimeoutEventListener {
+
+    /**
+     * Handle a timeout event.
+     *
+     * @param currentTime
+     *            Time to use as current time for any check.
+     * @return The delay to wait before next timeout callback in milliseconds,
+     *         or zero if this listener should no longer be notified.
+     */
+    long handleTimeout(final long currentTime);
+
+    /**
+     * Returns the timeout for this listener.
+     *
+     * @return The timeout in milliseconds.
+     */
+    long getTimeout();
+}
diff --git a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/AsynchronousFutureResult.java b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/AsynchronousFutureResult.java
index efa811d..77b0588 100644
--- a/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/AsynchronousFutureResult.java
+++ b/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/AsynchronousFutureResult.java
@@ -22,6 +22,7 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
+ *      Portions copyright 2013-2014 ForgeRock AS.
  */
 
 package com.forgerock.opendj.util;
@@ -161,34 +162,36 @@
             return getState() > 1;
         }
 
-        void innerSetErrorResult(final ErrorResultException errorResult) {
-            if (setStatePending()) {
-                this.errorResult = errorResult;
-
-                try {
-                    // Invoke error result completion handler.
-                    if (handler != null) {
-                        handler.handleErrorResult(errorResult);
-                    }
-                } finally {
-                    releaseShared(FAIL); // Publishes errorResult.
-                }
+        boolean innerSetErrorResult(final ErrorResultException errorResult) {
+            if (!setStatePending()) {
+                return false;
             }
+            this.errorResult = errorResult;
+            try {
+                // Invoke error result completion handler.
+                if (handler != null) {
+                    handler.handleErrorResult(errorResult);
+                }
+            } finally {
+                releaseShared(FAIL); // Publishes errorResult.
+            }
+            return true;
         }
 
-        void innerSetResult(final M result) {
-            if (setStatePending()) {
-                this.result = result;
-
-                try {
-                    // Invoke result completion handler.
-                    if (handler != null) {
-                        handler.handleResult(result);
-                    }
-                } finally {
-                    releaseShared(SUCCESS); // Publishes result.
-                }
+        boolean innerSetResult(final M result) {
+            if (!setStatePending()) {
+                return false;
             }
+            this.result = result;
+            try {
+                // Invoke result completion handler.
+                if (handler != null) {
+                    handler.handleResult(result);
+                }
+            } finally {
+                releaseShared(SUCCESS); // Publishes result.
+            }
+            return true;
         }
 
         private M get0() throws ErrorResultException {
@@ -323,6 +326,42 @@
     }
 
     /**
+     * Attempts to set the error result associated with this future. If (i.e.
+     * {@code isDone() == true}) then the error result will be ignored and
+     * {@code false} will be returned, otherwise the result handler will be
+     * invoked if one was provided and, on returning {@code true}, any threads
+     * waiting on {@link #get} will be released and the provided error result
+     * will be thrown.
+     *
+     * @param errorResult
+     *            The error result.
+     * @return {@code false} if this future has already been completed, either
+     *         due to normal termination, an exception, or cancellation (i.e.
+     *         {@code isDone() == true}).
+     */
+    public final boolean tryHandleErrorResult(final ErrorResultException errorResult) {
+        return sync.innerSetErrorResult(errorResult);
+    }
+
+    /**
+     * Attempts to set the result associated with this future. If (i.e.
+     * {@code isDone() == true}) then the result will be ignored and
+     * {@code false} will be returned, otherwise the result handler will be
+     * invoked if one was provided and, on returning {@code true}, any threads
+     * waiting on {@link #get} will be released and the provided result will be
+     * returned.
+     *
+     * @param result
+     *            The result.
+     * @return {@code false} if this future has already been completed, either
+     *         due to normal termination, an exception, or cancellation (i.e.
+     *         {@code isDone() == true}).
+     */
+    public final boolean tryHandleResult(final M result) {
+        return sync.innerSetResult(result);
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
diff --git a/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java b/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java
new file mode 100644
index 0000000..7dac273
--- /dev/null
+++ b/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java
@@ -0,0 +1,285 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *      Copyright 2014 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap;
+
+import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
+
+import com.forgerock.opendj.util.Validator;
+
+/**
+ * Common options for LDAP clients and listeners.
+ */
+abstract class CommonLDAPOptions<T extends CommonLDAPOptions<T>> {
+    // Default values for options taken from Java properties.
+    private static final boolean DEFAULT_TCP_NO_DELAY;
+    private static final boolean DEFAULT_REUSE_ADDRESS;
+    private static final boolean DEFAULT_KEEPALIVE;
+    private static final int DEFAULT_LINGER;
+    static {
+        DEFAULT_LINGER = getIntProperty("org.forgerock.opendj.io.linger", -1);
+        DEFAULT_TCP_NO_DELAY = getBooleanProperty("org.forgerock.opendj.io.tcpNoDelay", true);
+        DEFAULT_REUSE_ADDRESS = getBooleanProperty("org.forgerock.opendj.io.reuseAddress", true);
+        DEFAULT_KEEPALIVE = getBooleanProperty("org.forgerock.opendj.io.keepAlive", true);
+    }
+
+    static boolean getBooleanProperty(final String name, final boolean defaultValue) {
+        final String value = System.getProperty(name);
+        return value != null ? Boolean.parseBoolean(value) : defaultValue;
+    }
+
+    static int getIntProperty(final String name, final int defaultValue) {
+        final String value = System.getProperty(name);
+        try {
+            return value != null ? Integer.parseInt(value) : defaultValue;
+        } catch (final NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    private DecodeOptions decodeOptions;
+    private boolean tcpNoDelay = DEFAULT_TCP_NO_DELAY;
+    private boolean keepAlive = DEFAULT_KEEPALIVE;
+    private boolean reuseAddress = DEFAULT_REUSE_ADDRESS;
+    private int linger = DEFAULT_LINGER;
+    private TCPNIOTransport transport;
+
+    CommonLDAPOptions() {
+        this.decodeOptions = new DecodeOptions();
+    }
+
+    CommonLDAPOptions(final CommonLDAPOptions<?> options) {
+        this.decodeOptions = new DecodeOptions(options.decodeOptions);
+        this.linger = options.linger;
+        this.keepAlive = options.keepAlive;
+        this.reuseAddress = options.reuseAddress;
+        this.tcpNoDelay = options.tcpNoDelay;
+    }
+
+    /**
+     * Returns the decoding options which will be used to control how requests
+     * and responses are decoded.
+     *
+     * @return The decoding options which will be used to control how requests
+     *         and responses are decoded (never {@code null}).
+     */
+    public DecodeOptions getDecodeOptions() {
+        return decodeOptions;
+    }
+
+    /**
+     * Returns the value of the {@link java.net.StandardSocketOptions#SO_LINGER
+     * SO_LINGER} socket option for new connections.
+     * <p>
+     * The default setting is {@code -1} (disabled) and may be configured using
+     * the {@code org.forgerock.opendj.io.linger} property.
+     *
+     * @return The value of the {@link java.net.StandardSocketOptions#SO_LINGER
+     *         SO_LINGER} socket option for new connections, or -1 if linger
+     *         should be disabled.
+     */
+    public int getLinger() {
+        return linger;
+    }
+
+    /**
+     * Returns the value of the
+     * {@link java.net.StandardSocketOptions#SO_KEEPALIVE SO_KEEPALIVE} socket
+     * option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.keepAlive} property.
+     *
+     * @return The value of the
+     *         {@link java.net.StandardSocketOptions#SO_KEEPALIVE SO_KEEPALIVE}
+     *         socket option for new connections.
+     */
+    public boolean isKeepAlive() {
+        return keepAlive;
+    }
+
+    /**
+     * Returns the value of the
+     * {@link java.net.StandardSocketOptions#SO_REUSEADDR SO_REUSEADDR} socket
+     * option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.reuseAddress} property.
+     *
+     * @return The value of the
+     *         {@link java.net.StandardSocketOptions#SO_REUSEADDR SO_REUSEADDR}
+     *         socket option for new connections.
+     */
+    public boolean isReuseAddress() {
+        return reuseAddress;
+    }
+
+    /**
+     * Returns the value of the
+     * {@link java.net.StandardSocketOptions#TCP_NODELAY TCP_NODELAY} socket
+     * option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.tcpNoDelay} property.
+     *
+     * @return The value of the
+     *         {@link java.net.StandardSocketOptions#TCP_NODELAY TCP_NODELAY}
+     *         socket option for new connections.
+     */
+    public boolean isTCPNoDelay() {
+        return tcpNoDelay;
+    }
+
+    /**
+     * Sets the decoding options which will be used to control how requests and
+     * responses are decoded.
+     *
+     * @param decodeOptions
+     *            The decoding options which will be used to control how
+     *            requests and responses are decoded (never {@code null}).
+     * @return A reference to this set of options.
+     * @throws NullPointerException
+     *             If {@code decodeOptions} was {@code null}.
+     */
+    public T setDecodeOptions(final DecodeOptions decodeOptions) {
+        Validator.ensureNotNull(decodeOptions);
+        this.decodeOptions = decodeOptions;
+        return getThis();
+    }
+
+    /**
+     * Specifies the value of the
+     * {@link java.net.StandardSocketOptions#SO_KEEPALIVE SO_KEEPALIVE} socket
+     * option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.keepAlive} property.
+     *
+     * @param keepAlive
+     *            The value of the
+     *            {@link java.net.StandardSocketOptions#SO_KEEPALIVE
+     *            SO_KEEPALIVE} socket option for new connections.
+     * @return A reference to this set of options.
+     */
+    public T setKeepAlive(final boolean keepAlive) {
+        this.keepAlive = keepAlive;
+        return getThis();
+    }
+
+    /**
+     * Specifies the value of the
+     * {@link java.net.StandardSocketOptions#SO_LINGER SO_LINGER} socket option
+     * for new connections.
+     * <p>
+     * The default setting is {@code -1} (disabled) and may be configured using
+     * the {@code org.forgerock.opendj.io.linger} property.
+     *
+     * @param linger
+     *            The value of the
+     *            {@link java.net.StandardSocketOptions#SO_LINGER SO_LINGER}
+     *            socket option for new connections, or -1 if linger should be
+     *            disabled.
+     * @return A reference to this set of options.
+     */
+    public T setLinger(final int linger) {
+        this.linger = linger;
+        return getThis();
+    }
+
+    /**
+     * Specifies the value of the
+     * {@link java.net.StandardSocketOptions#SO_REUSEADDR SO_REUSEADDR} socket
+     * option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.reuseAddress} property.
+     *
+     * @param reuseAddress
+     *            The value of the
+     *            {@link java.net.StandardSocketOptions#SO_REUSEADDR
+     *            SO_REUSEADDR} socket option for new connections.
+     * @return A reference to this set of options.
+     */
+    public T setReuseAddress(final boolean reuseAddress) {
+        this.reuseAddress = reuseAddress;
+        return getThis();
+    }
+
+    /**
+     * Specifies the value of the
+     * {@link java.net.StandardSocketOptions#TCP_NODELAY TCP_NODELAY} socket
+     * option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.tcpNoDelay} property.
+     *
+     * @param tcpNoDelay
+     *            The value of the
+     *            {@link java.net.StandardSocketOptions#TCP_NODELAY TCP_NODELAY}
+     *            socket option for new connections.
+     * @return A reference to this set of options.
+     */
+    public T setTCPNoDelay(final boolean tcpNoDelay) {
+        this.tcpNoDelay = tcpNoDelay;
+        return getThis();
+    }
+
+    /**
+     * Returns the Grizzly TCP transport which will be used when initiating
+     * connections with the Directory Server.
+     * <p>
+     * By default this method will return {@code null} indicating that the
+     * default transport factory should be used to obtain a TCP transport.
+     *
+     * @return The Grizzly TCP transport which will be used when initiating
+     *         connections with the Directory Server, or {@code null} if the
+     *         default transport factory should be used to obtain a TCP
+     *         transport.
+     */
+    public TCPNIOTransport getTCPNIOTransport() {
+        return transport;
+    }
+
+    /**
+     * Sets the Grizzly TCP transport which will be used when initiating
+     * connections with the Directory Server.
+     * <p>
+     * By default this method will return {@code null} indicating that the
+     * default transport factory should be used to obtain a TCP transport.
+     *
+     * @param transport
+     *            The Grizzly TCP transport which will be used when initiating
+     *            connections with the Directory Server, or {@code null} if the
+     *            default transport factory should be used to obtain a TCP
+     *            transport.
+     * @return A reference to this connection options.
+     */
+    public T setTCPNIOTransport(final TCPNIOTransport transport) {
+        this.transport = transport;
+        return getThis();
+    }
+
+    abstract T getThis();
+}
diff --git a/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/LDAPListenerOptions.java b/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/LDAPListenerOptions.java
index 3290349..ff06b2c 100644
--- a/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/LDAPListenerOptions.java
+++ b/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/LDAPListenerOptions.java
@@ -22,34 +22,26 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2014 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap;
 
 import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
 
-import com.forgerock.opendj.util.Validator;
-
 /**
  * Common options for LDAP listeners.
  */
-public final class LDAPListenerOptions {
-
+public final class LDAPListenerOptions extends CommonLDAPOptions<LDAPListenerOptions> {
     private int backlog;
-    private DecodeOptions decodeOptions;
     private int maxRequestSize;
-    private TCPNIOTransport transport;
 
     /**
      * Creates a new set of listener options with default settings. SSL will not
      * be enabled, and a default set of decode options will be used.
      */
     public LDAPListenerOptions() {
-        this.backlog = 0;
-        this.maxRequestSize = 0;
-        this.decodeOptions = new DecodeOptions();
-        this.transport = null;
+        super();
     }
 
     /**
@@ -60,10 +52,9 @@
      *            The set of listener options to be copied.
      */
     public LDAPListenerOptions(final LDAPListenerOptions options) {
+        super(options);
         this.backlog = options.backlog;
         this.maxRequestSize = options.maxRequestSize;
-        this.decodeOptions = new DecodeOptions(options.decodeOptions);
-        this.transport = options.transport;
     }
 
     /**
@@ -79,17 +70,6 @@
     }
 
     /**
-     * Returns the decoding options which will be used to control how requests
-     * and responses are decoded.
-     *
-     * @return The decoding options which will be used to control how requests
-     *         and responses are decoded (never {@code null}).
-     */
-    public DecodeOptions getDecodeOptions() {
-        return decodeOptions;
-    }
-
-    /**
      * Returns the maximum request size in bytes for incoming LDAP requests. If
      * an incoming request exceeds the limit then the connection will be aborted
      * by the listener. If the limit is less than {@code 1} then a default value
@@ -102,22 +82,6 @@
     }
 
     /**
-     * Returns the Grizzly TCP transport which will be used when initiating
-     * connections with the Directory Server.
-     * <p>
-     * By default this method will return {@code null} indicating that the
-     * default transport factory should be used to obtain a TCP transport.
-     *
-     * @return The Grizzly TCP transport which will be used when initiating
-     *         connections with the Directory Server, or {@code null} if the
-     *         default transport factory should be used to obtain a TCP
-     *         transport.
-     */
-    public TCPNIOTransport getTCPNIOTransport() {
-        return transport;
-    }
-
-    /**
      * Sets the maximum queue length for incoming connections requests. If a
      * connection request arrives when the queue is full, the connection is
      * refused. If the backlog is less than {@code 1} then a default value of
@@ -133,23 +97,6 @@
     }
 
     /**
-     * Sets the decoding options which will be used to control how requests and
-     * responses are decoded.
-     *
-     * @param decodeOptions
-     *            The decoding options which will be used to control how
-     *            requests and responses are decoded (never {@code null}).
-     * @return A reference to this LDAP listener options.
-     * @throws NullPointerException
-     *             If {@code decodeOptions} was {@code null}.
-     */
-    public LDAPListenerOptions setDecodeOptions(final DecodeOptions decodeOptions) {
-        Validator.ensureNotNull(decodeOptions);
-        this.decodeOptions = decodeOptions;
-        return this;
-    }
-
-    /**
      * Sets the maximum request size in bytes for incoming LDAP requests. If an
      * incoming request exceeds the limit then the connection will be aborted by
      * the listener. If the limit is less than {@code 1} then a default value of
@@ -165,22 +112,25 @@
     }
 
     /**
-     * Sets the Grizzly TCP transport which will be used when initiating
-     * connections with the Directory Server.
-     * <p>
-     * By default this method will return {@code null} indicating that the
-     * default transport factory should be used to obtain a TCP transport.
-     *
-     * @param transport
-     *            The Grizzly TCP transport which will be used when initiating
-     *            connections with the Directory Server, or {@code null} if the
-     *            default transport factory should be used to obtain a TCP
-     *            transport.
-     * @return A reference to this connection options.
+     * {@inheritDoc}
      */
-    public LDAPListenerOptions setTCPNIOTransport(final TCPNIOTransport transport) {
-        this.transport = transport;
-        return this;
+    @Override
+    public LDAPListenerOptions setDecodeOptions(DecodeOptions decodeOptions) {
+        // This method is required for binary compatibility.
+        return super.setDecodeOptions(decodeOptions);
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public LDAPListenerOptions setTCPNIOTransport(TCPNIOTransport transport) {
+        // This method is required for binary compatibility.
+        return super.setTCPNIOTransport(transport);
+    }
+
+    @Override
+    LDAPListenerOptions getThis() {
+        return this;
+    }
 }
diff --git a/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/LDAPOptions.java b/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/LDAPOptions.java
index 4a8e8fc..f34c0a5 100644
--- a/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/LDAPOptions.java
+++ b/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/LDAPOptions.java
@@ -22,11 +22,13 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2014 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap;
 
+import static com.forgerock.opendj.util.Validator.ensureNotNull;
+
 import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -35,8 +37,6 @@
 
 import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
 
-import com.forgerock.opendj.util.Validator;
-
 /**
  * Common options for LDAP client connections.
  * <p>
@@ -56,25 +56,28 @@
  * // Connection uses StartTLS...
  * </pre>
  */
-public final class LDAPOptions {
+public final class LDAPOptions extends CommonLDAPOptions<LDAPOptions> {
+    // Default values for options taken from Java properties.
+    private static final long DEFAULT_TIMEOUT;
+    private static final long DEFAULT_CONNECT_TIMEOUT;
+    static {
+        DEFAULT_TIMEOUT = getIntProperty("org.forgerock.opendj.io.timeout", 0);
+        DEFAULT_CONNECT_TIMEOUT = getIntProperty("org.forgerock.opendj.io.connectTimeout", 5000);
+    }
+
     private SSLContext sslContext;
     private boolean useStartTLS;
-    private long timeoutInMillis;
-    private DecodeOptions decodeOptions;
-    private List<String> enabledCipherSuites = new LinkedList<String>();
-    private List<String> enabledProtocols = new LinkedList<String>();
-    private TCPNIOTransport transport;
+    private long timeoutInMillis = DEFAULT_TIMEOUT;
+    private long connectTimeoutInMillis = DEFAULT_CONNECT_TIMEOUT;
+    private final List<String> enabledCipherSuites = new LinkedList<String>();
+    private final List<String> enabledProtocols = new LinkedList<String>();
 
     /**
      * Creates a new set of connection options with default settings. SSL will
      * not be enabled, and a default set of decode options will be used.
      */
     public LDAPOptions() {
-        this.sslContext = null;
-        this.timeoutInMillis = 0;
-        this.useStartTLS = false;
-        this.decodeOptions = new DecodeOptions();
-        this.transport = null;
+        super();
     }
 
     /**
@@ -85,24 +88,93 @@
      *            The set of connection options to be copied.
      */
     public LDAPOptions(final LDAPOptions options) {
+        super(options);
         this.sslContext = options.sslContext;
         this.timeoutInMillis = options.timeoutInMillis;
         this.useStartTLS = options.useStartTLS;
-        this.decodeOptions = new DecodeOptions(options.decodeOptions);
         this.enabledCipherSuites.addAll(options.getEnabledCipherSuites());
         this.enabledProtocols.addAll(options.getEnabledProtocols());
-        this.transport = options.transport;
+        this.connectTimeoutInMillis = options.connectTimeoutInMillis;
     }
 
     /**
-     * Returns the decoding options which will be used to control how requests
-     * and responses are decoded.
+     * Adds the cipher suites enabled for secure connections with the Directory
+     * Server.
+     * <p>
+     * The suites must be supported by the SSLContext specified in
+     * {@link #setSSLContext(SSLContext)}. Following a successful call to this
+     * method, only the suites listed in the protocols parameter are enabled for
+     * use.
      *
-     * @return The decoding options which will be used to control how requests
-     *         and responses are decoded (never {@code null}).
+     * @param suites
+     *            Names of all the suites to enable.
+     * @return A reference to this set of options.
      */
-    public final DecodeOptions getDecodeOptions() {
-        return decodeOptions;
+    public LDAPOptions addEnabledCipherSuite(final String... suites) {
+        for (final String suite : suites) {
+            enabledCipherSuites.add(ensureNotNull(suite));
+        }
+        return this;
+    }
+
+    /**
+     * Adds the protocol versions enabled for secure connections with the
+     * Directory Server.
+     * <p>
+     * The protocols must be supported by the SSLContext specified in
+     * {@link #setSSLContext(SSLContext)}. Following a successful call to this
+     * method, only the protocols listed in the protocols parameter are enabled
+     * for use.
+     *
+     * @param protocols
+     *            Names of all the protocols to enable.
+     * @return A reference to this set of options.
+     */
+    public LDAPOptions addEnabledProtocol(final String... protocols) {
+        for (final String protocol : protocols) {
+            enabledProtocols.add(ensureNotNull(protocol));
+        }
+        return this;
+    }
+
+    /**
+     * Returns the connect timeout in the specified unit. If a connection is not
+     * established within the timeout period, then a
+     * {@link TimeoutResultException} error result will be returned. A timeout
+     * setting of 0 causes the OS connect timeout to be used.
+     * <p>
+     * The default operation timeout is 10 seconds and may be configured using
+     * the {@code org.forgerock.opendj.io.connectTimeout} property.
+     *
+     * @param unit
+     *            The time unit.
+     * @return The connect timeout, which may be 0 if there is no connect
+     *         timeout.
+     */
+    public long getConnectTimeout(final TimeUnit unit) {
+        return unit.convert(connectTimeoutInMillis, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Returns the names of the protocol versions which are currently enabled
+     * for secure connections with the Directory Server.
+     *
+     * @return An array of protocols or empty set if the default protocols are
+     *         to be used.
+     */
+    public List<String> getEnabledCipherSuites() {
+        return enabledCipherSuites;
+    }
+
+    /**
+     * Returns the names of the protocol versions which are currently enabled
+     * for secure connections with the Directory Server.
+     *
+     * @return An array of protocols or empty set if the default protocols are
+     *         to be used.
+     */
+    public List<String> getEnabledProtocols() {
+        return enabledProtocols;
     }
 
     /**
@@ -118,51 +190,47 @@
      *         connections with the Directory Server, which may be {@code null}
      *         indicating that connections will not be secured.
      */
-    public final SSLContext getSSLContext() {
+    public SSLContext getSSLContext() {
         return sslContext;
     }
 
     /**
-     * Returns the Grizzly TCP transport which will be used when initiating
-     * connections with the Directory Server.
+     * Returns the operation timeout in the specified unit. If a response is not
+     * received from the Directory Server within the timeout period, then the
+     * operation will be abandoned and a {@link TimeoutResultException} error
+     * result returned. A timeout setting of 0 disables operation timeout
+     * limits.
      * <p>
-     * By default this method will return {@code null} indicating that the
-     * default transport factory should be used to obtain a TCP transport.
-     *
-     * @return The Grizzly TCP transport which will be used when initiating
-     *         connections with the Directory Server, or {@code null} if the
-     *         default transport factory should be used to obtain a TCP
-     *         transport.
-     */
-    public final TCPNIOTransport getTCPNIOTransport() {
-        return transport;
-    }
-
-    /**
-     * Returns the operation timeout in the specified unit.
+     * The default operation timeout is 0 (no timeout) and may be configured
+     * using the {@code org.forgerock.opendj.io.timeout} property.
      *
      * @param unit
-     *            The time unit of use.
-     * @return The operation timeout.
+     *            The time unit.
+     * @return The operation timeout, which may be 0 if there is no operation
+     *         timeout.
      */
-    public final long getTimeout(final TimeUnit unit) {
+    public long getTimeout(final TimeUnit unit) {
         return unit.convert(timeoutInMillis, TimeUnit.MILLISECONDS);
     }
 
     /**
-     * Sets the decoding options which will be used to control how requests and
-     * responses are decoded.
+     * Sets the connect timeout. If a connection is not established within the
+     * timeout period, then a {@link TimeoutResultException} error result will
+     * be returned. A timeout setting of 0 causes the OS connect timeout to be
+     * used.
+     * <p>
+     * The default operation timeout is 10 seconds and may be configured using
+     * the {@code org.forgerock.opendj.io.connectTimeout} property.
      *
-     * @param decodeOptions
-     *            The decoding options which will be used to control how
-     *            requests and responses are decoded (never {@code null}).
-     * @return A reference to this LDAP connection options.
-     * @throws NullPointerException
-     *             If {@code decodeOptions} was {@code null}.
+     * @param timeout
+     *            The connect timeout, which may be 0 if there is no connect
+     *            timeout.
+     * @param unit
+     *            The time unit.
+     * @return A reference to this set of options.
      */
-    public final LDAPOptions setDecodeOptions(final DecodeOptions decodeOptions) {
-        Validator.ensureNotNull(decodeOptions);
-        this.decodeOptions = decodeOptions;
+    public LDAPOptions setConnectTimeout(final long timeout, final TimeUnit unit) {
+        this.connectTimeoutInMillis = unit.toMillis(timeout);
         return this;
     }
 
@@ -179,45 +247,30 @@
      *            The SSL context which will be used when initiating secure
      *            connections with the Directory Server, which may be
      *            {@code null} indicating that connections will not be secured.
-     * @return A reference to this LDAP connection options.
+     * @return A reference to this set of options.
      */
-    public final LDAPOptions setSSLContext(final SSLContext sslContext) {
+    public LDAPOptions setSSLContext(final SSLContext sslContext) {
         this.sslContext = sslContext;
         return this;
     }
 
     /**
-     * Sets the Grizzly TCP transport which will be used when initiating
-     * connections with the Directory Server.
+     * Sets the operation timeout. If a response is not received from the
+     * Directory Server within the timeout period, then the operation will be
+     * abandoned and a {@link TimeoutResultException} error result returned. A
+     * timeout setting of 0 disables operation timeout limits.
      * <p>
-     * By default this method will return {@code null} indicating that the
-     * default transport factory will be used to obtain a TCP transport.
-     *
-     * @param transport
-     *            The Grizzly TCP transport which will be used when initiating
-     *            connections with the Directory Server, or {@code null} if the
-     *            default transport factory should be used to obtain a TCP
-     *            transport.
-     * @return A reference to this LDAP connection options.
-     */
-    public final LDAPOptions setTCPNIOTransport(final TCPNIOTransport transport) {
-        this.transport = transport;
-        return this;
-    }
-
-    /**
-     * Sets the operation timeout. If the response is not received from the
-     * Directory Server in the timeout period, the operation will be abandoned
-     * and an error result returned. A timeout setting of 0 disables timeout
-     * limits.
+     * The default operation timeout is 0 (no timeout) and may be configured
+     * using the {@code org.forgerock.opendj.io.timeout} property.
      *
      * @param timeout
-     *            The operation timeout to use.
+     *            The operation timeout, which may be 0 if there is no operation
+     *            timeout.
      * @param unit
-     *            the time unit of the time argument.
-     * @return A reference to this LDAP connection options.
+     *            The time unit.
+     * @return A reference to this set of options.
      */
-    public final LDAPOptions setTimeout(final long timeout, final TimeUnit unit) {
+    public LDAPOptions setTimeout(final long timeout, final TimeUnit unit) {
         this.timeoutInMillis = unit.toMillis(timeout);
         return this;
     }
@@ -232,9 +285,9 @@
      *            {@code true} if StartTLS should be used for securing
      *            connections when an SSL context is specified, otherwise
      *            {@code false} indicating that SSL should be used.
-     * @return A reference to this LDAP connection options.
+     * @return A reference to this set of options.
      */
-    public final LDAPOptions setUseStartTLS(final boolean useStartTLS) {
+    public LDAPOptions setUseStartTLS(final boolean useStartTLS) {
         this.useStartTLS = useStartTLS;
         return this;
     }
@@ -249,70 +302,30 @@
      *         when an SSL context is specified, otherwise {@code false}
      *         indicating that SSL should be used.
      */
-    public final boolean useStartTLS() {
+    public boolean useStartTLS() {
         return useStartTLS;
     }
 
     /**
-     * Adds the protocol versions enabled for secure connections with the
-     * Directory Server.
-     * <p>
-     * The protocols must be supported by the SSLContext specified in
-     * {@link #setSSLContext(SSLContext)}. Following a successful call to this
-     * method, only the protocols listed in the protocols parameter are enabled
-     * for use.
-     *
-     * @param protocols
-     *            Names of all the protocols to enable.
-     * @return A reference to this LDAP connection options.
+     * {@inheritDoc}
      */
-    public final LDAPOptions addEnabledProtocol(String... protocols) {
-        for (final String protocol : protocols) {
-            enabledProtocols.add(Validator.ensureNotNull(protocol));
-        }
+    @Override
+    public LDAPOptions setDecodeOptions(DecodeOptions decodeOptions) {
+        // This method is required for binary compatibility.
+        return super.setDecodeOptions(decodeOptions);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public LDAPOptions setTCPNIOTransport(TCPNIOTransport transport) {
+        // This method is required for binary compatibility.
+        return super.setTCPNIOTransport(transport);
+    }
+
+    @Override
+    LDAPOptions getThis() {
         return this;
     }
-
-    /**
-     * Adds the cipher suites enabled for secure connections with the Directory
-     * Server.
-     * <p>
-     * The suites must be supported by the SSLContext specified in
-     * {@link #setSSLContext(SSLContext)}. Following a successful call to this
-     * method, only the suites listed in the protocols parameter are enabled for
-     * use.
-     *
-     * @param suites
-     *            Names of all the suites to enable.
-     * @return A reference to this LDAP connection options.
-     */
-    public final LDAPOptions addEnabledCipherSuite(String... suites) {
-        for (final String suite : suites) {
-            enabledCipherSuites.add(Validator.ensureNotNull(suite));
-        }
-        return this;
-    }
-
-    /**
-     * Returns the names of the protocol versions which are currently enabled
-     * for secure connections with the Directory Server.
-     *
-     * @return An array of protocols or empty set if the default protocols are
-     *         to be used.
-     */
-    public final List<String> getEnabledProtocols() {
-        return enabledProtocols;
-    }
-
-    /**
-     * Returns the names of the protocol versions which are currently enabled
-     * for secure connections with the Directory Server.
-     *
-     * @return An array of protocols or empty set if the default protocols are
-     *         to be used.
-     */
-    public final List<String> getEnabledCipherSuites() {
-        return enabledCipherSuites;
-    }
-
 }
diff --git a/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties b/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
index 16fae6d..d3cb3ab 100755
--- a/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
+++ b/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
@@ -1423,6 +1423,8 @@
 HBCF_HEARTBEAT_TIMEOUT=Heartbeat timed out after %d ms
 LDAP_CONNECTION_REQUEST_TIMEOUT=The request has failed because no response \
  was received from the server within the %d ms timeout
+LDAP_CONNECTION_CONNECT_TIMEOUT=The connection attempt to server %s has failed \
+ because the connection timeout period of %d ms was exceeded
 LDAP_CONNECTION_BIND_OR_START_TLS_REQUEST_TIMEOUT=The bind or StartTLS request \
  has failed because no response was received from the server within the %d ms \
  timeout. The LDAP connection is now in an invalid state and can no longer be used
diff --git a/opendj-ldap-sdk/src/test/java/com/forgerock/opendj/ldap/LDAPConnectionTestCase.java b/opendj-ldap-sdk/src/test/java/com/forgerock/opendj/ldap/LDAPConnectionTestCase.java
index 1c1d13a..93ac8c5 100644
--- a/opendj-ldap-sdk/src/test/java/com/forgerock/opendj/ldap/LDAPConnectionTestCase.java
+++ b/opendj-ldap-sdk/src/test/java/com/forgerock/opendj/ldap/LDAPConnectionTestCase.java
@@ -104,7 +104,7 @@
             connection.searchAsync(request, null, handler);
 
             // Pass in a time which is guaranteed to trigger expiration.
-            connection.cancelExpiredRequests(System.currentTimeMillis() + 1000000);
+            connection.handleTimeout(System.currentTimeMillis() + 1000000);
             if (isPersistentSearch) {
                 verifyZeroInteractions(handler);
             } else {
diff --git a/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPConnectionFactoryTestCase.java b/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPConnectionFactoryTestCase.java
index 7778e46..1fbf160 100644
--- a/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPConnectionFactoryTestCase.java
+++ b/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPConnectionFactoryTestCase.java
@@ -21,7 +21,7 @@
  * CDDL HEADER END
  *
  *
- *     Copyright 2013 ForgeRock AS.
+ *     Copyright 2013-2014 ForgeRock AS.
  */
 package org.forgerock.opendj.ldap;
 
@@ -61,7 +61,11 @@
  */
 @SuppressWarnings({ "javadoc", "unchecked" })
 public class LDAPConnectionFactoryTestCase extends SdkTestCase {
-    // Manual testing has gone up to 10000 iterations.
+    /*
+     * The number of test iterations for unit tests which attempt to expose
+     * potential race conditions. Manual testing has gone up to 10000
+     * iterations.
+     */
     private static final int ITERATIONS = 100;
 
     // Test timeout for tests which need to wait for network events.
@@ -95,6 +99,31 @@
         server.close();
     }
 
+    @Test(description = "OPENDJ-1197")
+    public void testClientSideConnectTimeout() throws Exception {
+        // Use an non-local unreachable network address.
+        final ConnectionFactory factory =
+                new LDAPConnectionFactory("10.20.30.40", 1389, new LDAPOptions().setConnectTimeout(
+                        1, TimeUnit.MILLISECONDS));
+        try {
+            for (int i = 0; i < ITERATIONS; i++) {
+                final ResultHandler<Connection> handler = mock(ResultHandler.class);
+                final FutureResult<Connection> future = factory.getConnectionAsync(handler);
+                // Wait for the connect to timeout.
+                try {
+                    future.get(TEST_TIMEOUT, TimeUnit.SECONDS);
+                    fail("The connect request succeeded unexpectedly");
+                } catch (ConnectionException e) {
+                    assertThat(e.getResult().getResultCode()).isEqualTo(
+                            ResultCode.CLIENT_SIDE_CONNECT_ERROR);
+                    verify(handler).handleErrorResult(same(e));
+                }
+            }
+        } finally {
+            factory.close();
+        }
+    }
+
     /**
      * Unit test for OPENDJ-1247: a locally timed out bind request will leave a
      * connection in an invalid state since a bind (or startTLS) is in progress

--
Gitblit v1.10.0