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