| | |
| | | */ |
| | | package org.forgerock.opendj.ldap; |
| | | |
| | | import static com.forgerock.opendj.util.StaticUtils.closeSilently; |
| | | import static org.fest.assertions.Assertions.assertThat; |
| | | import static org.fest.assertions.Fail.fail; |
| | | import static org.forgerock.opendj.ldap.TestCaseUtils.findFreeSocketAddress; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest; |
| | | import static org.mockito.Mockito.*; |
| | | import static org.mockito.Matchers.any; |
| | | import static org.mockito.Matchers.anyInt; |
| | | import static org.mockito.Matchers.same; |
| | | import static org.mockito.Mockito.doAnswer; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.never; |
| | | import static org.mockito.Mockito.verify; |
| | | import static org.mockito.Mockito.verifyZeroInteractions; |
| | | |
| | | import java.io.IOException; |
| | | import java.util.concurrent.Semaphore; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.TimeoutException; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.opendj.ldap.requests.AbandonRequest; |
| | |
| | | import org.forgerock.opendj.ldap.requests.UnbindRequest; |
| | | import org.forgerock.opendj.ldap.responses.BindResult; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.mockito.ArgumentCaptor; |
| | | import org.mockito.invocation.InvocationOnMock; |
| | | import org.mockito.stubbing.Answer; |
| | | import org.mockito.stubbing.Stubber; |
| | | import org.testng.annotations.AfterClass; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | |
| | | */ |
| | | @SuppressWarnings({ "javadoc", "unchecked" }) |
| | | public class LDAPConnectionFactoryTestCase extends SdkTestCase { |
| | | // 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 AtomicReference<LDAPClientContext> context = |
| | | new AtomicReference<LDAPClientContext>(); |
| | | private volatile ServerConnection<Integer> serverConnection; |
| | | private final LDAPListener server = createServer(); |
| | | private final ConnectionFactory factory = new LDAPConnectionFactory(server.getSocketAddress(), |
| | | new LDAPOptions().setTimeout(1, TimeUnit.MILLISECONDS)); |
| | | private final ConnectionFactory pool = Connections.newFixedConnectionPool(factory, 10); |
| | | |
| | | private final Semaphore connectLatch = new Semaphore(0); |
| | | private final Semaphore abandonLatch = new Semaphore(0); |
| | | private final Semaphore bindLatch = new Semaphore(0); |
| | | private final Semaphore searchLatch = new Semaphore(0); |
| | | private final Semaphore closeLatch = new Semaphore(0); |
| | | |
| | | @AfterClass |
| | | public void tearDown() { |
| | | pool.close(); |
| | | factory.close(); |
| | | server.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 |
| | |
| | | */ |
| | | @Test |
| | | public void testClientSideTimeoutForBindRequest() throws Exception { |
| | | final AtomicReference<LDAPClientContext> context = new AtomicReference<LDAPClientContext>(); |
| | | final Semaphore latch = new Semaphore(0); |
| | | resetState(); |
| | | registerBindEvent(); |
| | | registerCloseEvent(); |
| | | |
| | | // The server connection should receive a bind, but no abandon request. |
| | | final ServerConnection<Integer> serverConnection = mock(ServerConnection.class); |
| | | release(latch).when(serverConnection).handleBind(any(Integer.class), anyInt(), |
| | | any(BindRequest.class), any(IntermediateResponseHandler.class), |
| | | any(ResultHandler.class)); |
| | | release(latch).when(serverConnection).handleConnectionClosed(any(Integer.class), |
| | | any(UnbindRequest.class)); |
| | | |
| | | final LDAPListener server = createServer(latch, context, serverConnection); |
| | | final ConnectionFactory factory = |
| | | new LDAPConnectionFactory(server.getSocketAddress(), new LDAPOptions().setTimeout( |
| | | 1, TimeUnit.MILLISECONDS)); |
| | | Connection connection = null; |
| | | for (int i = 0; i < ITERATIONS; i++) { |
| | | final Connection connection = factory.getConnection(); |
| | | try { |
| | | // Connect to the server. |
| | | connection = factory.getConnection(); |
| | | |
| | | // Wait for the server to accept the connection. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | |
| | | /* |
| | | * A bind request timeout should cause the connection to fail, so |
| | | * ensure that event listeners are fired. |
| | | */ |
| | | final ConnectionEventListener listener = mock(ConnectionEventListener.class); |
| | | waitForConnect(); |
| | | final MockConnectionEventListener listener = new MockConnectionEventListener(); |
| | | connection.addConnectionEventListener(listener); |
| | | |
| | | final ResultHandler<BindResult> handler = mock(ResultHandler.class); |
| | | final FutureResult<BindResult> future = |
| | | connection.bindAsync(newSimpleBindRequest(), null, handler); |
| | | |
| | | // Wait for the server to receive the bind request. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | waitForBind(); |
| | | |
| | | // Wait for the request to timeout. |
| | | try { |
| | | future.get(TEST_TIMEOUT, TimeUnit.SECONDS); |
| | | fail("The bind request succeeded unexpectedly"); |
| | | } catch (TimeoutResultException e) { |
| | | assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_TIMEOUT); |
| | | verifyResultCodeIsClientSideTimeout(e); |
| | | verify(handler).handleErrorResult(same(e)); |
| | | |
| | | // The connection should no longer be valid. |
| | | ArgumentCaptor<ErrorResultException> capturedError = |
| | | ArgumentCaptor.forClass(ErrorResultException.class); |
| | | verify(listener).handleConnectionError(eq(false), capturedError.capture()); |
| | | assertThat(capturedError.getValue().getResult().getResultCode()).isEqualTo( |
| | | ResultCode.CLIENT_SIDE_TIMEOUT); |
| | | assertThat(connection.isValid()).isFalse(); |
| | | connection.close(); |
| | | |
| | | // Wait for the server to receive the close request. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | |
| | | /* |
| | | * Check that the only interactions were the bind and the close |
| | | * and specifically there was no abandon request. |
| | | * The connection should no longer be valid, the event |
| | | * listener should have been notified, but no abandon should |
| | | * have been sent. |
| | | */ |
| | | verify(serverConnection).handleBind(any(Integer.class), eq(3), |
| | | any(BindRequest.class), any(IntermediateResponseHandler.class), |
| | | any(ResultHandler.class)); |
| | | verify(serverConnection).handleConnectionClosed(any(Integer.class), |
| | | any(UnbindRequest.class)); |
| | | verifyNoMoreInteractions(serverConnection); |
| | | listener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS); |
| | | assertThat(connection.isValid()).isFalse(); |
| | | verifyResultCodeIsClientSideTimeout(listener.getError()); |
| | | connection.close(); |
| | | waitForClose(); |
| | | verifyNoAbandonSent(); |
| | | } |
| | | } finally { |
| | | closeSilently(connection); |
| | | factory.close(); |
| | | server.close(); |
| | | connection.close(); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | */ |
| | | @Test |
| | | public void testClientSideTimeoutForBindRequestInConnectionPool() throws Exception { |
| | | final AtomicReference<LDAPClientContext> context = new AtomicReference<LDAPClientContext>(); |
| | | final Semaphore latch = new Semaphore(0); |
| | | resetState(); |
| | | registerBindEvent(); |
| | | registerCloseEvent(); |
| | | |
| | | // The server connection should receive a bind, but no abandon request. |
| | | final ServerConnection<Integer> serverConnection = mock(ServerConnection.class); |
| | | release(latch).when(serverConnection).handleBind(any(Integer.class), anyInt(), |
| | | any(BindRequest.class), any(IntermediateResponseHandler.class), |
| | | any(ResultHandler.class)); |
| | | release(latch).when(serverConnection).handleConnectionClosed(any(Integer.class), |
| | | any(UnbindRequest.class)); |
| | | |
| | | final LDAPListener server = createServer(latch, context, serverConnection); |
| | | final ConnectionFactory factory = |
| | | Connections.newFixedConnectionPool( |
| | | new LDAPConnectionFactory(server.getSocketAddress(), new LDAPOptions() |
| | | .setTimeout(1, TimeUnit.MILLISECONDS)), 10); |
| | | Connection connection = null; |
| | | for (int i = 0; i < ITERATIONS; i++) { |
| | | final Connection connection = pool.getConnection(); |
| | | try { |
| | | // Get pooled connection to the server. |
| | | connection = factory.getConnection(); |
| | | |
| | | // Wait for the server to accept the connection. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | |
| | | /* |
| | | * Sanity check: close the connection and reopen. There should be no |
| | | * interactions with the server due to the pool. |
| | | */ |
| | | connection.close(); |
| | | connection = factory.getConnection(); |
| | | verifyNoMoreInteractions(serverConnection); |
| | | waitForConnect(); |
| | | final MockConnectionEventListener listener = new MockConnectionEventListener(); |
| | | connection.addConnectionEventListener(listener); |
| | | |
| | | // Now bind with timeout. |
| | | final ResultHandler<BindResult> handler = mock(ResultHandler.class); |
| | | final FutureResult<BindResult> future = |
| | | connection.bindAsync(newSimpleBindRequest(), null, handler); |
| | | |
| | | // Wait for the server to receive the bind request. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | waitForBind(); |
| | | |
| | | // Wait for the request to timeout. |
| | | try { |
| | | future.get(TEST_TIMEOUT, TimeUnit.SECONDS); |
| | | future.get(5, TimeUnit.SECONDS); |
| | | fail("The bind request succeeded unexpectedly"); |
| | | } catch (TimeoutException e) { |
| | | fail("The bind request future get timed out"); |
| | | } catch (TimeoutResultException e) { |
| | | assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_TIMEOUT); |
| | | verifyResultCodeIsClientSideTimeout(e); |
| | | verify(handler).handleErrorResult(same(e)); |
| | | |
| | | // The connection should no longer be valid. |
| | | assertThat(connection.isValid()).isFalse(); |
| | | connection.close(); |
| | | |
| | | // Wait for the server to receive the close request. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | |
| | | /* |
| | | * Check that the only interactions were the bind and the close |
| | | * and specifically there was no abandon request. |
| | | * The connection should no longer be valid, the event |
| | | * listener should have been notified, but no abandon should |
| | | * have been sent. |
| | | */ |
| | | verify(serverConnection).handleBind(any(Integer.class), eq(3), |
| | | any(BindRequest.class), any(IntermediateResponseHandler.class), |
| | | any(ResultHandler.class)); |
| | | verify(serverConnection).handleConnectionClosed(any(Integer.class), |
| | | any(UnbindRequest.class)); |
| | | verifyNoMoreInteractions(serverConnection); |
| | | |
| | | // Now get another connection. This time we should reconnect to the server. |
| | | connection = factory.getConnection(); |
| | | |
| | | // Wait for the server to accept the connection. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | listener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS); |
| | | assertThat(connection.isValid()).isFalse(); |
| | | verifyResultCodeIsClientSideTimeout(listener.getError()); |
| | | connection.close(); |
| | | waitForClose(); |
| | | verifyNoAbandonSent(); |
| | | } |
| | | } finally { |
| | | closeSilently(connection); |
| | | factory.close(); |
| | | server.close(); |
| | | connection.close(); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | */ |
| | | @Test |
| | | public void testClientSideTimeoutForSearchRequest() throws Exception { |
| | | final AtomicReference<LDAPClientContext> context = new AtomicReference<LDAPClientContext>(); |
| | | final Semaphore latch = new Semaphore(0); |
| | | resetState(); |
| | | registerSearchEvent(); |
| | | registerAbandonEvent(); |
| | | |
| | | // The server connection should receive a search and then an abandon. |
| | | final ServerConnection<Integer> serverConnection = mock(ServerConnection.class); |
| | | release(latch).when(serverConnection).handleSearch(any(Integer.class), |
| | | any(SearchRequest.class), any(IntermediateResponseHandler.class), |
| | | any(SearchResultHandler.class)); |
| | | release(latch).when(serverConnection).handleAbandon(any(Integer.class), |
| | | any(AbandonRequest.class)); |
| | | |
| | | final LDAPListener server = createServer(latch, context, serverConnection); |
| | | final ConnectionFactory factory = |
| | | new LDAPConnectionFactory(server.getSocketAddress(), new LDAPOptions().setTimeout( |
| | | 1, TimeUnit.MILLISECONDS)); |
| | | Connection connection = null; |
| | | for (int i = 0; i < ITERATIONS; i++) { |
| | | final Connection connection = factory.getConnection(); |
| | | try { |
| | | // Connect to the server. |
| | | connection = factory.getConnection(); |
| | | |
| | | // Wait for the server to accept the connection. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | |
| | | /* |
| | | * A search request timeout should not cause the connection to fail, |
| | | * so ensure that event listeners are not fired. |
| | | */ |
| | | waitForConnect(); |
| | | final ConnectionEventListener listener = mock(ConnectionEventListener.class); |
| | | connection.addConnectionEventListener(listener); |
| | | |
| | | final ResultHandler<SearchResultEntry> handler = mock(ResultHandler.class); |
| | | final FutureResult<SearchResultEntry> future = |
| | | connection.readEntryAsync(DN.valueOf("cn=test"), null, handler); |
| | | |
| | | // Wait for the server to receive the search request. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | waitForSearch(); |
| | | |
| | | // Wait for the request to timeout. |
| | | try { |
| | | future.get(TEST_TIMEOUT, TimeUnit.SECONDS); |
| | | fail("The search request succeeded unexpectedly"); |
| | | } catch (TimeoutResultException e) { |
| | | assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_TIMEOUT); |
| | | verifyResultCodeIsClientSideTimeout(e); |
| | | verify(handler).handleErrorResult(same(e)); |
| | | |
| | | // The connection should still be valid. |
| | | verifyZeroInteractions(listener); |
| | | assertThat(connection.isValid()).isTrue(); |
| | | verifyZeroInteractions(listener); |
| | | |
| | | // Wait for the server to receive the abandon request. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | /* |
| | | * FIXME: The search should have been abandoned (see comment |
| | | * in LDAPConnection for explanation). |
| | | */ |
| | | // waitForAbandon(); |
| | | } |
| | | } finally { |
| | | closeSilently(connection); |
| | | factory.close(); |
| | | server.close(); |
| | | connection.close(); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | */ |
| | | @Test |
| | | public void testResourceManagement() throws Exception { |
| | | final AtomicReference<LDAPClientContext> context = new AtomicReference<LDAPClientContext>(); |
| | | final Semaphore latch = new Semaphore(0); |
| | | final LDAPListener server = createServer(latch, context, mock(ServerConnection.class)); |
| | | final ConnectionFactory factory = new LDAPConnectionFactory(server.getSocketAddress()); |
| | | try { |
| | | for (int i = 0; i < 100; i++) { |
| | | // Connect to the server. |
| | | resetState(); |
| | | |
| | | for (int i = 0; i < ITERATIONS; i++) { |
| | | final Connection connection = factory.getConnection(); |
| | | try { |
| | | // Wait for the server to accept the connection. |
| | | assertThat(latch.tryAcquire(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue(); |
| | | |
| | | waitForConnect(); |
| | | final MockConnectionEventListener listener = new MockConnectionEventListener(); |
| | | connection.addConnectionEventListener(listener); |
| | | |
| | |
| | | connection.close(); |
| | | } |
| | | } |
| | | } finally { |
| | | factory.close(); |
| | | server.close(); |
| | | } |
| | | } |
| | | |
| | | private LDAPListener createServer(final Semaphore latch, |
| | | final AtomicReference<LDAPClientContext> context, |
| | | final ServerConnection<Integer> serverConnection) throws IOException { |
| | | private LDAPListener createServer() { |
| | | try { |
| | | return new LDAPListener(findFreeSocketAddress(), |
| | | new ServerConnectionFactory<LDAPClientContext, Integer>() { |
| | | @Override |
| | | public ServerConnection<Integer> handleAccept( |
| | | final LDAPClientContext clientContext) throws ErrorResultException { |
| | | context.set(clientContext); |
| | | latch.release(); |
| | | connectLatch.release(); |
| | | return serverConnection; |
| | | } |
| | | }); |
| | | } catch (IOException e) { |
| | | fail("Unable to create LDAP listener", e); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private Stubber release(final Semaphore latch) { |
| | | private Stubber notifyEvent(final Semaphore latch) { |
| | | return doAnswer(new Answer<Void>() { |
| | | @Override |
| | | public Void answer(InvocationOnMock invocation) throws Throwable { |
| | | 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)); |
| | | } |
| | | |
| | | 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(ErrorResultException 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); |
| | | } |
| | | } |