private void doTestRequestTimeout(boolean isPersistentSearch) throws Exception {
    InetSocketAddress address = TestCaseUtils.findFreeSocketAddress();

    /*
     * Use a mock server implementation which will ignore incoming requests
     * and leave the client waiting forever for a response.
     */
    @SuppressWarnings("unchecked")
    LDAPListener listener =
        new LDAPListener(
            address, Connections.newServerConnectionFactory(mock(RequestHandler.class)));

    /*
     * Use a very long time out in order to prevent the timeout thread from
     * triggering the timeout.
     */
    LDAPConnectionFactory factory =
        new LDAPConnectionFactory(
            address.getHostName(),
            address.getPort(),
            Options.defaultOptions()
                .set(TIMEOUT_IN_MILLISECONDS, TimeUnit.SECONDS.toMillis((long) 100)));
    GrizzlyLDAPConnection connection = (GrizzlyLDAPConnection) factory.getConnection();
    try {
      SearchRequest request =
          Requests.newSearchRequest("dc=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
      if (isPersistentSearch) {
        request.addControl(PersistentSearchRequestControl.newControl(true, true, true));
      }
      SearchResultHandler searchHandler = mock(SearchResultHandler.class);
      @SuppressWarnings("unchecked")
      ExceptionHandler<LdapException> exceptionHandler = mock(ExceptionHandler.class);
      connection.searchAsync(request, searchHandler).thenOnException(exceptionHandler);

      // Pass in a time which is guaranteed to trigger expiration.
      connection.handleTimeout(System.currentTimeMillis() + 1000000);
      if (isPersistentSearch) {
        verifyZeroInteractions(searchHandler);
      } else {
        ArgumentCaptor<LdapException> arg = ArgumentCaptor.forClass(LdapException.class);
        verify(exceptionHandler).handleException(arg.capture());
        assertThat(arg.getValue()).isInstanceOf(TimeoutResultException.class);
        assertThat(arg.getValue().getResult().getResultCode())
            .isEqualTo(ResultCode.CLIENT_SIDE_TIMEOUT);
      }
    } finally {
      connection.close();
      listener.close();
      factory.close();
    }
  }
/** Tests the {@link LDAPConnectionFactory} class. */
@SuppressWarnings({"javadoc", "unchecked"})
public class GrizzlyLDAPConnectionFactoryTestCase extends SdkTestCase {
  /**
   * 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. */
  private static final long TEST_TIMEOUT = 30L;

  /*
   * It is usually quite a bad code smell to share state between unit tests.
   * However, in this case we want to re-use the same factories and listeners
   * in order to avoid shutting down and restarting the transport for each
   * iteration.
   */

  private final Semaphore abandonLatch = new Semaphore(0);
  private final Semaphore bindLatch = new Semaphore(0);
  private final Semaphore closeLatch = new Semaphore(0);
  private final Semaphore connectLatch = new Semaphore(0);
  private final Semaphore searchLatch = new Semaphore(0);
  private final AtomicReference<LDAPClientContext> context = new AtomicReference<>();
  private final LDAPListener server = createServer();
  private final InetSocketAddress socketAddress = server.getSocketAddress();
  private final ConnectionFactory factory =
      new LDAPConnectionFactory(
          socketAddress.getHostName(),
          socketAddress.getPort(),
          new LDAPOptions().setTimeout(1, TimeUnit.MILLISECONDS));
  private final ConnectionFactory pool = Connections.newFixedConnectionPool(factory, 10);
  private volatile ServerConnection<Integer> serverConnection;

  @AfterClass
  public void tearDown() {
    pool.close();
    factory.close();
    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 PromiseImpl<LdapException, NeverThrowsException> promise = PromiseImpl.create();
        final Promise<? extends Connection, LdapException> connectionPromise =
            factory.getConnectionAsync();
        connectionPromise.onFailure(getFailureHandler(promise));

        ConnectionException e =
            (ConnectionException) promise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
        assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_CONNECT_ERROR);
        // Wait for the connect to timeout.
        try {
          connectionPromise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
          fail("The connect request succeeded unexpectedly");
        } catch (ConnectionException ce) {
          assertThat(ce.getResult().getResultCode())
              .isEqualTo(ResultCode.CLIENT_SIDE_CONNECT_ERROR);
        }
      }
    } 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 and no other operations can be
   * performed. Therefore, a timeout should cause the connection to become invalid and an
   * appropriate connection event sent. In addition, no abandon request should be sent.
   */
  @Test
  public void testClientSideTimeoutForBindRequest() throws Exception {
    resetState();
    registerBindEvent();
    registerCloseEvent();

    final Connection connection = factory.getConnection();
    try {
      waitForConnect();
      final MockConnectionEventListener listener = new MockConnectionEventListener();
      connection.addConnectionEventListener(listener);
      final PromiseImpl<LdapException, NeverThrowsException> promise = PromiseImpl.create();
      final LdapPromise<BindResult> bindPromise = connection.bindAsync(newSimpleBindRequest());
      bindPromise.onFailure(getFailureHandler(promise));
      waitForBind();

      TimeoutResultException e =
          (TimeoutResultException) promise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
      verifyResultCodeIsClientSideTimeout(e);

      // Wait for the request to timeout.
      try {
        bindPromise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
        fail("The bind request succeeded unexpectedly");
      } catch (TimeoutResultException te) {
        verifyResultCodeIsClientSideTimeout(te);
      }

      /*
       * The connection should no longer be valid, the event listener
       * should have been notified, but no abandon should have been sent.
       */
      listener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS);
      assertThat(connection.isValid()).isFalse();
      verifyResultCodeIsClientSideTimeout(listener.getError());
      connection.close();
      waitForClose();
      verifyNoAbandonSent();
    } finally {
      connection.close();
    }
  }

  /**
   * Unit test for OPENDJ-1247: as per previous test, except this time verify that the connection
   * failure removes the connection from a connection pool.
   */
  @Test
  public void testClientSideTimeoutForBindRequestInConnectionPool() throws Exception {
    resetState();
    registerBindEvent();
    registerCloseEvent();

    for (int i = 0; i < ITERATIONS; i++) {
      final Connection connection = pool.getConnection();
      try {
        waitForConnect();
        final MockConnectionEventListener listener = new MockConnectionEventListener();
        connection.addConnectionEventListener(listener);

        // Now bind with timeout.
        final PromiseImpl<LdapException, NeverThrowsException> promise = PromiseImpl.create();
        final LdapPromise<BindResult> bindPromise = connection.bindAsync(newSimpleBindRequest());
        bindPromise.onFailure(getFailureHandler(promise));
        waitForBind();

        // Wait for the request to timeout and check the handler was invoked.
        TimeoutResultException e = (TimeoutResultException) promise.getOrThrow(5, TimeUnit.SECONDS);
        verifyResultCodeIsClientSideTimeout(e);

        // Now check the promise was completed as expected.
        try {
          bindPromise.getOrThrow(5, TimeUnit.SECONDS);
          fail("The bind request succeeded unexpectedly");
        } catch (TimeoutResultException te) {
          verifyResultCodeIsClientSideTimeout(te);
        }

        /*
         * The connection should no longer be valid, the event listener
         * should have been notified, but no abandon should have been
         * sent.
         */
        listener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS);
        assertThat(connection.isValid()).isFalse();
        verifyResultCodeIsClientSideTimeout(listener.getError());
        connection.close();
        waitForClose();
        verifyNoAbandonSent();
      } finally {
        connection.close();
      }
    }
  }

  /**
   * Unit test for OPENDJ-1247: a locally timed out request which is not a bind or startTLS should
   * result in a client side timeout error, but the connection should remain valid. In addition, no
   * abandon request should be sent.
   */
  @Test
  public void testClientSideTimeoutForSearchRequest() throws Exception {
    resetState();
    registerSearchEvent();
    registerAbandonEvent();

    for (int i = 0; i < ITERATIONS; i++) {
      final Connection connection = factory.getConnection();
      try {
        waitForConnect();
        final ConnectionEventListener listener = mock(ConnectionEventListener.class);
        connection.addConnectionEventListener(listener);
        final PromiseImpl<LdapException, NeverThrowsException> promise = PromiseImpl.create();
        final LdapPromise<SearchResultEntry> connectionPromise =
            connection.readEntryAsync(DN.valueOf("cn=test"), null);
        connectionPromise.onFailure(getFailureHandler(promise));
        waitForSearch();

        LdapException e = promise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
        verifyResultCodeIsClientSideTimeout(e);
        // Wait for the request to timeout.
        try {
          connectionPromise.getOrThrow(TEST_TIMEOUT, TimeUnit.SECONDS);
          fail("The search request succeeded unexpectedly");
        } catch (TimeoutResultException te) {
          verifyResultCodeIsClientSideTimeout(te);
        }

        // The connection should still be valid.
        assertThat(connection.isValid()).isTrue();
        verifyZeroInteractions(listener);
        /*
         * FIXME: The search should have been abandoned (see comment in
         * LDAPConnection for explanation).
         */
        // waitForAbandon();
      } finally {
        connection.close();
      }
    }
  }

  @Test
  public void testCreateLDAPConnectionFactory() throws Exception {
    // test no exception is thrown, which means transport provider is correctly loaded
    InetSocketAddress socketAddress = findFreeSocketAddress();
    LDAPConnectionFactory factory =
        new LDAPConnectionFactory(socketAddress.getHostName(), socketAddress.getPort());
    factory.close();
  }

  @Test(
      expectedExceptions = {ProviderNotFoundException.class},
      expectedExceptionsMessageRegExp = "^The requested provider 'unknown' .*")
  public void testCreateLDAPConnectionFactoryFailureProviderNotFound() throws Exception {
    LDAPOptions options = new LDAPOptions().setTransportProvider("unknown");
    InetSocketAddress socketAddress = findFreeSocketAddress();
    LDAPConnectionFactory factory =
        new LDAPConnectionFactory(socketAddress.getHostName(), socketAddress.getPort(), options);
    factory.close();
  }

  @Test
  public void testCreateLDAPConnectionFactoryWithCustomClassLoader() throws Exception {
    // test no exception is thrown, which means transport provider is correctly loaded
    LDAPOptions options =
        new LDAPOptions().setProviderClassLoader(Thread.currentThread().getContextClassLoader());
    InetSocketAddress socketAddress = findFreeSocketAddress();
    LDAPConnectionFactory factory =
        new LDAPConnectionFactory(socketAddress.getHostName(), socketAddress.getPort(), options);
    factory.close();
  }

  /**
   * This unit test exposes the bug raised in issue OPENDJ-1156: NPE in ReferenceCountedObject after
   * shutting down directory.
   */
  @Test
  public void testResourceManagement() throws Exception {
    resetState();

    for (int i = 0; i < ITERATIONS; i++) {
      final Connection connection = factory.getConnection();
      try {
        waitForConnect();
        final MockConnectionEventListener listener = new MockConnectionEventListener();
        connection.addConnectionEventListener(listener);

        // Perform remote disconnect which will trigger a client side connection error.
        context.get().disconnect();

        // Wait for the error notification to reach the client.
        listener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS);
      } finally {
        connection.close();
      }
    }
  }

  private LDAPListener createServer() {
    try {
      return new LDAPListener(
          findFreeSocketAddress(),
          new ServerConnectionFactory<LDAPClientContext, Integer>() {
            @Override
            public ServerConnection<Integer> handleAccept(final LDAPClientContext clientContext)
                throws LdapException {
              context.set(clientContext);
              connectLatch.release();
              return serverConnection;
            }
          });
    } catch (IOException e) {
      fail("Unable to create LDAP listener", e);
      return null;
    }
  }

  private FailureHandler<LdapException> getFailureHandler(
      final PromiseImpl<LdapException, NeverThrowsException> promise) {
    return new FailureHandler<LdapException>() {
      @Override
      public void handleError(LdapException error) {
        promise.handleResult(error);
      }
    };
  }

  private Stubber notifyEvent(final Semaphore latch) {
    return doAnswer(
        new Answer<Void>() {
          @Override
          public Void answer(InvocationOnMock invocation) {
            latch.release();
            return null;
          }
        });
  }

  private void registerAbandonEvent() {
    notifyEvent(abandonLatch)
        .when(serverConnection)
        .handleAbandon(any(Integer.class), any(AbandonRequest.class));
  }

  private void registerBindEvent() {
    notifyEvent(bindLatch)
        .when(serverConnection)
        .handleBind(
            any(Integer.class),
            anyInt(),
            any(BindRequest.class),
            any(IntermediateResponseHandler.class),
            any(ResultHandler.class));
  }

  private void registerCloseEvent() {
    notifyEvent(closeLatch)
        .when(serverConnection)
        .handleConnectionClosed(any(Integer.class), any(UnbindRequest.class));
  }

  private void registerSearchEvent() {
    notifyEvent(searchLatch)
        .when(serverConnection)
        .handleSearch(
            any(Integer.class),
            any(SearchRequest.class),
            any(IntermediateResponseHandler.class),
            any(SearchResultHandler.class),
            any(ResultHandler.class));
  }

  private void resetState() {
    connectLatch.drainPermits();
    abandonLatch.drainPermits();
    bindLatch.drainPermits();
    searchLatch.drainPermits();
    closeLatch.drainPermits();
    context.set(null);
    serverConnection = mock(ServerConnection.class);
  }

  private void verifyNoAbandonSent() {
    verify(serverConnection, never()).handleAbandon(any(Integer.class), any(AbandonRequest.class));
  }

  private void verifyResultCodeIsClientSideTimeout(LdapException error) {
    assertThat(error.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_TIMEOUT);
  }

  @SuppressWarnings("unused")
  private void waitForAbandon() throws InterruptedException {
    waitForEvent(abandonLatch);
  }

  private void waitForBind() throws InterruptedException {
    waitForEvent(bindLatch);
  }

  private void waitForClose() throws InterruptedException {
    waitForEvent(closeLatch);
  }

  private void waitForConnect() throws InterruptedException {
    waitForEvent(connectLatch);
  }

  private void waitForEvent(final Semaphore latch) throws InterruptedException {
    assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue();
  }

  private void waitForSearch() throws InterruptedException {
    waitForEvent(searchLatch);
  }
}
 @AfterClass
 public void tearDown() {
   pool.close();
   factory.close();
   server.close();
 }