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(); }