From 7983705a0ce4de2fd6e7a4061fe4afa0f9e8c66a Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 12 Sep 2012 20:46:39 +0000
Subject: [PATCH] First part of fix for OPENDJ-590: ConnectionPool may return already closed/disconnected connections

---
 opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/ConnectionFactoryTestCase.java |  242 +++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 239 insertions(+), 3 deletions(-)

diff --git a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/ConnectionFactoryTestCase.java b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/ConnectionFactoryTestCase.java
index 9b6f243..19887b1 100644
--- a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/ConnectionFactoryTestCase.java
+++ b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/ConnectionFactoryTestCase.java
@@ -22,13 +22,15 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2011 ForgeRock AS
+ *      Portions copyright 2011-2012 ForgeRock AS
  */
 
 package org.forgerock.opendj.ldap;
 
 import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -37,16 +39,22 @@
 
 import java.net.InetSocketAddress;
 import java.util.Arrays;
+import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.logging.Level;
 
 import javax.net.ssl.SSLContext;
 
+import org.forgerock.opendj.ldap.requests.BindRequest;
 import org.forgerock.opendj.ldap.requests.DigestMD5SASLBindRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.Responses;
 import org.forgerock.opendj.ldap.responses.SearchResultEntry;
 import org.forgerock.opendj.ldap.schema.Schema;
 import org.forgerock.opendj.ldap.schema.SchemaBuilder;
@@ -65,6 +73,10 @@
  */
 @SuppressWarnings("javadoc")
 public class ConnectionFactoryTestCase extends SdkTestCase {
+    // Test timeout in ms for tests which need to wait for network events.
+    private static final long TEST_TIMEOUT = 30L;
+    private static final long TEST_TIMEOUT_MS = TEST_TIMEOUT * 1000L;
+
     class MyResultHandler implements ResultHandler<Connection> {
         // latch.
         private final CountDownLatch latch;
@@ -114,8 +126,8 @@
         StaticUtils.DEBUG_LOG.setLevel(Level.INFO);
     }
 
-    @DataProvider(name = "connectionFactories")
-    public Object[][] getConnectionFactories() throws Exception {
+    @DataProvider
+    Object[][] connectionFactories() throws Exception {
         Object[][] factories = new Object[21][1];
 
         // HeartBeatConnectionFactory
@@ -429,4 +441,228 @@
         assertThat(realConnectionIsClosed[2]).isTrue();
         assertThat(realConnectionIsClosed[3]).isTrue();
     }
+
+    private static final class CloseNotify {
+        private boolean closeOnAccept;
+        private boolean doBindFirst;
+        private boolean useEventListener;
+        private boolean sendDisconnectNotification;
+
+        private CloseNotify(boolean closeOnAccept, boolean doBindFirst, boolean useEventListener,
+                boolean sendDisconnectNotification) {
+            this.closeOnAccept = closeOnAccept;
+            this.doBindFirst = doBindFirst;
+            this.useEventListener = useEventListener;
+            this.sendDisconnectNotification = sendDisconnectNotification;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("[");
+            if (closeOnAccept) {
+                builder.append(" closeOnAccept");
+            }
+            if (doBindFirst) {
+                builder.append(" doBindFirst");
+            }
+            if (useEventListener) {
+                builder.append(" useEventListener");
+            }
+            if (sendDisconnectNotification) {
+                builder.append(" sendDisconnectNotification");
+            }
+            builder.append(" ]");
+            return builder.toString();
+        }
+    }
+
+    @DataProvider
+    Object[][] closeNotifyConfig() {
+        // @formatter:off
+        return new Object[][] {
+            // closeOnAccept, doBindFirst, useEventListener, sendDisconnectNotification
+
+            // Close on accept.
+            { new CloseNotify(true,  false, false, false) },
+            { new CloseNotify(true,  false, true,  false) },
+
+            // Use disconnect.
+            { new CloseNotify(false, false, false, false) },
+            { new CloseNotify(false, false, false, true) },
+            { new CloseNotify(false, false, true,  false) },
+            { new CloseNotify(false, false, true,  true) },
+            { new CloseNotify(false, true,  false, false) },
+            { new CloseNotify(false, true,  false, true) },
+            { new CloseNotify(false, true,  true,  false) },
+            { new CloseNotify(false, true,  true,  true) },
+        };
+        // @formatter:on
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test(dataProvider = "closeNotifyConfig")
+    public void testCloseNotify(final CloseNotify config) throws Exception {
+        final CountDownLatch connectLatch = new CountDownLatch(1);
+        final AtomicReference<LDAPClientContext> contextHolder =
+                new AtomicReference<LDAPClientContext>();
+
+        final ServerConnectionFactory<LDAPClientContext, Integer> mockServer =
+                mock(ServerConnectionFactory.class);
+        when(mockServer.handleAccept(any(LDAPClientContext.class))).thenAnswer(
+                new Answer<ServerConnection<Integer>>() {
+
+                    public ServerConnection<Integer> answer(InvocationOnMock invocation)
+                            throws Throwable {
+                        // Allow the context to be accessed from outside the mock.
+                        contextHolder.set((LDAPClientContext) invocation.getArguments()[0]);
+                        connectLatch.countDown(); /* is this needed? */
+                        if (config.closeOnAccept) {
+                            throw newErrorResult(ResultCode.UNAVAILABLE);
+                        } else {
+                            // Return a mock connection which always succeeds for binds.
+                            ServerConnection<Integer> mockConnection = mock(ServerConnection.class);
+                            doAnswer(new Answer<Void>() {
+                                @Override
+                                public Void answer(InvocationOnMock invocation) throws Throwable {
+                                    ResultHandler<? super BindResult> resultHandler =
+                                            (ResultHandler<? super BindResult>) invocation
+                                                    .getArguments()[4];
+                                    resultHandler.handleResult(Responses
+                                            .newBindResult(ResultCode.SUCCESS));
+                                    return null;
+                                }
+                            }).when(mockConnection).handleBind(anyInt(), anyInt(),
+                                    any(BindRequest.class), any(IntermediateResponseHandler.class),
+                                    any(ResultHandler.class));
+                            return mockConnection;
+                        }
+                    }
+                });
+
+        final int port = TestCaseUtils.findFreePort();
+        LDAPListener listener = new LDAPListener(port, mockServer);
+        try {
+            LDAPConnectionFactory clientFactory = new LDAPConnectionFactory("localhost", port);
+            final Connection client = clientFactory.getConnection();
+            connectLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
+            MockConnectionEventListener mockListener = null;
+            try {
+                if (config.useEventListener) {
+                    mockListener = new MockConnectionEventListener();
+                    client.addConnectionEventListener(mockListener);
+                }
+                if (config.doBindFirst) {
+                    client.bind("cn=test", "password".toCharArray());
+                }
+                if (!config.closeOnAccept) {
+                    // Disconnect using client context.
+                    LDAPClientContext context = contextHolder.get();
+                    assertThat(context).isNotNull();
+                    assertThat(context.isClosed()).isFalse();
+                    if (config.sendDisconnectNotification) {
+                        context.disconnect(ResultCode.BUSY, "busy");
+                    } else {
+                        context.disconnect();
+                    }
+                    assertThat(context.isClosed()).isTrue();
+                }
+                // Block until remote close is signalled.
+                if (mockListener != null) {
+                    // Block using listener.
+                    mockListener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS);
+                    assertThat(mockListener.getInvocationCount()).isEqualTo(1);
+                    assertThat(mockListener.isDisconnectNotification()).isEqualTo(
+                            config.sendDisconnectNotification);
+                    assertThat(mockListener.getError()).isNotNull();
+                } else {
+                    // Block by spinning on isValid.
+                    waitForCondition(new Callable<Boolean>() {
+                        @Override
+                        public Boolean call() throws Exception {
+                            return !client.isValid();
+                        }
+                    });
+                }
+                assertThat(client.isValid()).isFalse();
+                assertThat(client.isClosed()).isFalse();
+            } finally {
+                client.close();
+            }
+            // Check state after remote close and local close.
+            assertThat(client.isValid()).isFalse();
+            assertThat(client.isClosed()).isTrue();
+            if (mockListener != null) {
+                mockListener.awaitClose(TEST_TIMEOUT, TimeUnit.SECONDS);
+                assertThat(mockListener.getInvocationCount()).isEqualTo(2);
+            }
+        } finally {
+            listener.close();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testUnsolicitedNotifications() throws Exception {
+        final CountDownLatch connectLatch = new CountDownLatch(1);
+        final AtomicReference<LDAPClientContext> contextHolder =
+                new AtomicReference<LDAPClientContext>();
+
+        final ServerConnectionFactory<LDAPClientContext, Integer> mockServer =
+                mock(ServerConnectionFactory.class);
+        when(mockServer.handleAccept(any(LDAPClientContext.class))).thenAnswer(
+                new Answer<ServerConnection<Integer>>() {
+
+                    public ServerConnection<Integer> answer(InvocationOnMock invocation)
+                            throws Throwable {
+                        // Allow the context to be accessed from outside the mock.
+                        contextHolder.set((LDAPClientContext) invocation.getArguments()[0]);
+                        connectLatch.countDown(); /* is this needed? */
+                        return mock(ServerConnection.class);
+                    }
+                });
+
+        final int port = TestCaseUtils.findFreePort();
+        LDAPListener listener = new LDAPListener(port, mockServer);
+        try {
+            LDAPConnectionFactory clientFactory = new LDAPConnectionFactory("localhost", port);
+            final Connection client = clientFactory.getConnection();
+            connectLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS);
+            try {
+                MockConnectionEventListener mockListener = new MockConnectionEventListener();
+                client.addConnectionEventListener(mockListener);
+
+                // Send notification.
+                LDAPClientContext context = contextHolder.get();
+                assertThat(context).isNotNull();
+                context.sendUnsolicitedNotification(Responses.newGenericExtendedResult(
+                        ResultCode.OTHER).setOID("1.2.3.4"));
+                assertThat(context.isClosed()).isFalse();
+
+                // Block using listener.
+                mockListener.awaitNotification(TEST_TIMEOUT, TimeUnit.SECONDS);
+                assertThat(mockListener.getInvocationCount()).isEqualTo(1);
+                assertThat(mockListener.getNotification()).isNotNull();
+                assertThat(mockListener.getNotification().getResultCode()).isEqualTo(
+                        ResultCode.OTHER);
+                assertThat(mockListener.getNotification().getOID()).isEqualTo("1.2.3.4");
+                assertThat(client.isValid()).isTrue();
+                assertThat(client.isClosed()).isFalse();
+            } finally {
+                client.close();
+            }
+        } finally {
+            listener.close();
+        }
+    }
+
+    private void waitForCondition(Callable<Boolean> condition) throws Exception {
+        long timeout = System.currentTimeMillis() + TEST_TIMEOUT_MS;
+        while (!condition.call()) {
+            Thread.yield();
+            if (System.currentTimeMillis() > timeout) {
+                throw new TimeoutException("Test timed out after " + TEST_TIMEOUT + " seconds");
+            }
+        }
+    }
 }

--
Gitblit v1.10.0