// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or at: // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ================================================================================================= package com.twitter.common.thrift; import java.io.IOException; import java.net.InetSocketAddress; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import com.google.common.base.Function; import org.apache.thrift.TException; import org.apache.thrift.async.AsyncMethodCallback; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; import org.easymock.Capture; import org.easymock.EasyMock; import org.easymock.IAnswer; import org.easymock.IExpectationSetters; import org.easymock.IMocksControl; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import com.twitter.common.base.Command; import com.twitter.common.net.loadbalancing.LoadBalancer; import com.twitter.common.net.loadbalancing.RequestTracker; import com.twitter.common.net.pool.Connection; import com.twitter.common.net.pool.ObjectPool; import com.twitter.common.net.pool.ResourceExhaustedException; import com.twitter.common.quantity.Amount; import com.twitter.common.quantity.Time; import com.twitter.common.stats.Stat; import com.twitter.common.stats.Stats; import com.twitter.common.thrift.callers.RetryingCaller; import com.twitter.common.thrift.testing.MockTSocket; import com.twitter.common.util.concurrent.ForwardingExecutorService; import static org.easymock.EasyMock.and; import static org.easymock.EasyMock.anyLong; import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.capture; import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.isA; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * @author John Sirois */ public class ThriftTest { private static final Amount<Long, Time> ASYNC_CONNECT_TIMEOUT = Amount.of(1L, Time.SECONDS); public static class NotFoundException extends Exception {} public interface TestService { int calculateMass(String profileName) throws NotFoundException, TException; } public interface TestServiceAsync { void calculateMass(String profileName, AsyncMethodCallback callback) throws TException; } private IMocksControl control; private ObjectPool<Connection<TTransport, InetSocketAddress>> connectionPool; private Function<TTransport, TestService> clientFactory; private Function<TTransport, TestServiceAsync> asyncClientFactory; private RequestTracker<InetSocketAddress> requestTracker; private AsyncMethodCallback<Integer> callback; @SuppressWarnings("unchecked") @Before public void setUp() throws Exception { control = EasyMock.createControl(); this.connectionPool = control.createMock(ObjectPool.class); this.clientFactory = control.createMock(Function.class); this.asyncClientFactory = control.createMock(Function.class); this.requestTracker = control.createMock(LoadBalancer.class); this.callback = control.createMock(AsyncMethodCallback.class); } @After public void after() { Stats.flush(); } @Test public void testDoCallNoDeadline() throws Exception { TestService testService = expectServiceCall(false); expect(testService.calculateMass("jake")).andReturn(42); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.SUCCESS), anyLong()); Thrift<TestService> thrift = createThrift(expectUnusedExecutorService()); control.replay(); int userMass = thrift.builder().blocking().create().calculateMass("jake"); assertEquals(42, userMass); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 0); assertReconnectsTotal(thrift, 0); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testDoCallAsync() throws Exception { // Capture the callback that Thift has wrapped around our callback. Capture<AsyncMethodCallback<Integer>> callbackCapture = new Capture<AsyncMethodCallback<Integer>>(); expectAsyncServiceCall(false).calculateMass(eq("jake"), capture(callbackCapture)); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.SUCCESS), anyLong()); // Verifies that our callback was called. callback.onComplete(42); Thrift<TestServiceAsync> thrift = createAsyncThrift(expectUnusedExecutorService()); control.replay(); thrift.builder().withConnectTimeout(ASYNC_CONNECT_TIMEOUT).create() .calculateMass("jake", callback); // Mimicks the async response from the server. callbackCapture.getValue().onComplete(42); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 0); assertReconnectsTotal(thrift, 0); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testDoCallServiceException() throws Exception { TestService testService = expectServiceCall(true); NotFoundException notFoundException = new NotFoundException(); expect(testService.calculateMass("jake")).andThrow(notFoundException); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); Thrift<TestService> thrift = createThrift(expectUnusedExecutorService()); control.replay(); try { thrift.builder().blocking().create().calculateMass("jake"); fail("Expected service custom exception to bubble unmodified"); } catch (NotFoundException e) { assertSame(notFoundException, e); } assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 1); assertReconnectsTotal(thrift, 1); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testDoCallAsyncServiceException() throws Exception { NotFoundException notFoundException = new NotFoundException(); // Capture the callback that Thift has wrapped around our callback. Capture<AsyncMethodCallback<Integer>> callbackCapture = new Capture<AsyncMethodCallback<Integer>>(); expectAsyncServiceCall(true).calculateMass(eq("jake"), capture(callbackCapture)); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); // Verifies that our callback was called. callback.onError(notFoundException); Thrift<TestServiceAsync> thrift = createAsyncThrift(expectUnusedExecutorService()); control.replay(); thrift.builder().withConnectTimeout(ASYNC_CONNECT_TIMEOUT).create() .calculateMass("jake", callback); // Mimicks the async response from the server. callbackCapture.getValue().onError(notFoundException); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 1); assertReconnectsTotal(thrift, 1); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testDoCallThriftException() throws Exception { Capture<TTransport> transportCapture = new Capture<TTransport>(); TestService testService = expectThriftError(transportCapture); TTransportException tException = new TTransportException(); expect(testService.calculateMass("jake")).andThrow(tException); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); Thrift<TestService> thrift = createThrift(expectUnusedExecutorService()); control.replay(); try { thrift.builder().blocking().create().calculateMass("jake"); fail("Expected thrift exception to bubble unmodified"); } catch (TException e) { assertSame(tException, e); } assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 1); assertReconnectsTotal(thrift, 1); assertTimeoutsTotal(thrift, 0); assertTrue(transportCapture.hasCaptured()); assertFalse("Expected the transport to be forcibly closed when a thrift error is encountered", transportCapture.getValue().isOpen()); control.verify(); } @Test public void doCallAsyncThriftException() throws Exception { TTransportException tException = new TTransportException(); expectAsyncServiceCall(true).calculateMass(eq("jake"), (AsyncMethodCallback) anyObject()); expectLastCall().andThrow(tException); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); Thrift<TestServiceAsync> thrift = createAsyncThrift(expectUnusedExecutorService()); callback.onError(tException); control.replay(); thrift.builder().withConnectTimeout(ASYNC_CONNECT_TIMEOUT).create() .calculateMass("jake", callback); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 1); assertReconnectsTotal(thrift, 1); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test(expected = IllegalArgumentException.class) public void testDisallowsAsyncWithDeadline() { Config config = Config.builder() .withRequestTimeout(Amount.of(1L, Time.SECONDS)) .create(); new Thrift<TestServiceAsync>(config, connectionPool, requestTracker, "foo", TestServiceAsync.class, asyncClientFactory, true, false).create(); } @Test public void testDoCallDeadlineMet() throws Exception { TestService testService = expectServiceCall(false); expect(testService.calculateMass("jake")).andReturn(42); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.SUCCESS), anyLong()); ExecutorService executorService = Executors.newSingleThreadExecutor(); Thrift<TestService> thrift = createThrift(executorService); control.replay(); int userMass = thrift.builder().withRequestTimeout(Amount.of(1L, Time.DAYS)).create() .calculateMass("jake"); assertEquals(42, userMass); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 0); assertReconnectsTotal(thrift, 0); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test @Ignore("Flaky: https://trac.twitter.com/twttr/ticket/11474") public void testDoCallDeadlineExpired() throws Exception { TestService testService = expectServiceCall(true); // Setup a way to verify the callable was cancelled by Thrift when timeout elapsed final CountDownLatch remoteCallComplete = new CountDownLatch(1); final CountDownLatch remoteCallStarted = new CountDownLatch(1); final Command verifyCancelled = control.createMock(Command.class); verifyCancelled.execute(); final Object block = new Object(); expect(testService.calculateMass("jake")).andAnswer(new IAnswer<Integer>() { @Override public Integer answer() throws TException { try { synchronized (block) { remoteCallStarted.countDown(); block.wait(); } fail("Expected late work to be cancelled and interrupted"); } catch (InterruptedException e) { verifyCancelled.execute(); } finally { remoteCallComplete.countDown(); } throw new TTransportException(); } }); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.TIMEOUT), anyLong()); ExecutorService executorService = new ForwardingExecutorService<ExecutorService>(Executors.newSingleThreadExecutor()) { @Override public <T> Future<T> submit(Callable<T> task) { Future<T> future = super.submit(task); // make sure the task is started so we can verify it gets cancelled try { remoteCallStarted.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } return future; } }; Thrift<TestService> thrift = createThrift(executorService); control.replay(); try { thrift.builder().withRequestTimeout(Amount.of(1L, Time.NANOSECONDS)).create() .calculateMass("jake"); fail("Expected a timeout"); } catch (TTimeoutException e) { // expected } finally { remoteCallComplete.await(); } assertRequestsTotal(thrift, 0); assertErrorsTotal(thrift, 0); assertReconnectsTotal(thrift, 0); assertTimeoutsTotal(thrift, 1); control.verify(); } @Test public void testRetriesNoProblems() throws Exception { expect(expectServiceCall(false).calculateMass("jake")).andReturn(42); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.SUCCESS), anyLong()); Thrift<TestService> thrift = createThrift(expectUnusedExecutorService()); control.replay(); TestService testService = thrift.builder().blocking().withRetries(1).create(); assertEquals(42, testService.calculateMass("jake")); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 0); assertReconnectsTotal(thrift, 0); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testAsyncRetriesNoProblems() throws Exception { // Capture the callback that Thift has wrapped around our callback. Capture<AsyncMethodCallback<Integer>> callbackCapture = new Capture<AsyncMethodCallback<Integer>>(); expectAsyncServiceCall(false).calculateMass(eq("jake"), capture(callbackCapture)); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.SUCCESS), anyLong()); // Verifies that our callback was called. callback.onComplete(42); Thrift<TestServiceAsync> thrift = createAsyncThrift(expectUnusedExecutorService()); control.replay(); thrift.builder().withRetries(1).withConnectTimeout(ASYNC_CONNECT_TIMEOUT).create() .calculateMass("jake", callback); // Mimicks the async response from the server. callbackCapture.getValue().onComplete(42); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 0); assertReconnectsTotal(thrift, 0); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testRetriesRecover() throws Exception { // 1st call expect(expectServiceCall(true).calculateMass("jake")).andThrow(new TTransportException()); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); // 1st retry recovers expect(expectServiceCall(false).calculateMass("jake")).andReturn(42); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.SUCCESS), anyLong()); Thrift<TestService> thrift = createThrift(expectUnusedExecutorService()); control.replay(); TestService testService = thrift.builder().blocking().withRetries(1).create(); assertEquals(42, testService.calculateMass("jake")); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 0); assertReconnectsTotal(thrift, 0); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testAsyncRetriesRecover() throws Exception { // Capture the callback that Thift has wrapped around our callback. Capture<AsyncMethodCallback<Integer>> callbackCapture = new Capture<AsyncMethodCallback<Integer>>(); // 1st call expectAsyncServiceCall(true).calculateMass(eq("jake"), capture(callbackCapture)); expectLastCall().andThrow(new TTransportException()); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); // 1st retry recovers expectAsyncServiceRetry(false).calculateMass(eq("jake"), capture(callbackCapture)); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.SUCCESS), anyLong()); // Verifies that our callback was called. callback.onComplete(42); Thrift<TestServiceAsync> thrift = createAsyncThrift(expectUnusedExecutorService()); control.replay(); thrift.builder().withRetries(1).withConnectTimeout(ASYNC_CONNECT_TIMEOUT).create() .calculateMass("jake", callback); // Mimicks the async response from the server. callbackCapture.getValue().onComplete(42); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 0); assertReconnectsTotal(thrift, 0); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testRetriesFailure() throws Exception { // 1st call expect(expectServiceCall(true).calculateMass("jake")).andThrow(new TTransportException()); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); // 1st retry expect(expectServiceCall(true).calculateMass("jake")).andThrow(new TTransportException()); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); // 2nd retry TTransportException finalRetryException = new TTransportException(); expect(expectServiceCall(true).calculateMass("jake")).andThrow(finalRetryException); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); Thrift<TestService> thrift = createThrift(expectUnusedExecutorService()); control.replay(); TestService testService = thrift.builder().blocking().withRetries(2).create(); try { testService.calculateMass("jake"); fail("Expected an exception to be thrown since all retires failed"); } catch (TException e) { assertSame(finalRetryException, e); } assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 1); assertReconnectsTotal(thrift, 1); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testAsyncRetriesFailure() throws Exception { // 1st call Capture<AsyncMethodCallback<Integer>> callbackCapture1 = new Capture<AsyncMethodCallback<Integer>>(); expectAsyncServiceCall(true).calculateMass(eq("jake"), capture(callbackCapture1)); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); // 1st retry Capture<AsyncMethodCallback<Integer>> callbackCapture2 = new Capture<AsyncMethodCallback<Integer>>(); expectAsyncServiceRetry(true).calculateMass(eq("jake"), capture(callbackCapture2)); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); // 2nd retry Capture<AsyncMethodCallback<Integer>> callbackCapture3 = new Capture<AsyncMethodCallback<Integer>>(); expectAsyncServiceRetry(true).calculateMass(eq("jake"), capture(callbackCapture3)); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); // Verifies that our callback was called. TTransportException returnedException = new TTransportException(); callback.onError(returnedException); Thrift<TestServiceAsync> thrift = createAsyncThrift(expectUnusedExecutorService()); control.replay(); thrift.builder().withRetries(2).withConnectTimeout(ASYNC_CONNECT_TIMEOUT).create() .calculateMass("jake", callback); callbackCapture1.getValue().onError(new TTransportException()); callbackCapture2.getValue().onError(new IOException()); callbackCapture3.getValue().onError(returnedException); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 1); assertReconnectsTotal(thrift, 1); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testRetrySelection() throws Exception { expect(expectServiceCall(true).calculateMass("jake")).andThrow(new NotFoundException()); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); // verify subclasses pass the retry filter class HopelesslyLost extends NotFoundException {} expect(expectServiceCall(true).calculateMass("jake")).andThrow(new HopelesslyLost()); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); TTransportException nonRetryableException = new TTransportException(); expect(expectServiceCall(true).calculateMass("jake")).andThrow(nonRetryableException); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); Thrift<TestService> thrift = createThrift(expectUnusedExecutorService()); control.replay(); TestService testService = thrift.builder().blocking().withRetries(2).retryOn(NotFoundException.class).create(); try { testService.calculateMass("jake"); fail("Expected n exception to be thrown since all retires failed"); } catch (TException e) { assertSame(nonRetryableException, e); } assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 1); assertReconnectsTotal(thrift, 1); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testAsyncRetrySelection() throws Exception { // verify subclasses pass the retry filter class HopelesslyLost extends NotFoundException {} Capture<AsyncMethodCallback<Integer>> callbackCapture1 = new Capture<AsyncMethodCallback<Integer>>(); expectAsyncServiceCall(true).calculateMass(eq("jake"), capture(callbackCapture1)); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); Capture<AsyncMethodCallback<Integer>> callbackCapture2 = new Capture<AsyncMethodCallback<Integer>>(); expectAsyncServiceRetry(true).calculateMass(eq("jake"), capture(callbackCapture2)); requestTracker.requestResult( (InetSocketAddress) anyObject(), eq(RequestTracker.RequestResult.FAILED), anyLong()); // Verifies that our callback was called. TTransportException nonRetryableException = new TTransportException(); callback.onError(nonRetryableException); Thrift<TestServiceAsync> thrift = createAsyncThrift(expectUnusedExecutorService()); control.replay(); TestServiceAsync testService = thrift.builder() .withRetries(2) .retryOn(NotFoundException.class) .withConnectTimeout(ASYNC_CONNECT_TIMEOUT).create(); testService.calculateMass("jake", callback); callbackCapture1.getValue().onError(new HopelesslyLost()); callbackCapture2.getValue().onError(nonRetryableException); assertRequestsTotal(thrift, 1); assertErrorsTotal(thrift, 1); assertReconnectsTotal(thrift, 1); assertTimeoutsTotal(thrift, 0); control.verify(); } @Test public void testResourceExhausted() throws Exception { expectConnectionPoolResourceExhausted(Config.DEFAULT_CONNECT_TIMEOUT); Thrift<TestService> thrift = createThrift(expectUnusedExecutorService()); control.replay(); TestService testService = thrift.builder().blocking().create(); try { testService.calculateMass("jake"); fail("Expected a TResourceExhaustedException."); } catch (TResourceExhaustedException e) { // Expected } control.verify(); } @Test public void testAsyncResourceExhausted() throws Exception { expectConnectionPoolResourceExhausted(ASYNC_CONNECT_TIMEOUT); Thrift<TestServiceAsync> thrift = createAsyncThrift(expectUnusedExecutorService()); callback.onError((Throwable) and(anyObject(), isA(TResourceExhaustedException.class))); control.replay(); TestServiceAsync testService = thrift.builder().withConnectTimeout(ASYNC_CONNECT_TIMEOUT) .create(); testService.calculateMass("jake", callback); control.verify(); } @Test public void testAsyncDoesNotRetryResourceExhausted() throws Exception { expect(connectionPool.get(ASYNC_CONNECT_TIMEOUT)).andThrow( new ResourceExhaustedException("first")); Thrift<TestServiceAsync> thrift = createAsyncThrift(expectUnusedExecutorService()); callback.onError((Throwable) and(anyObject(), isA(TResourceExhaustedException.class))); control.replay(); thrift.builder().withRetries(1).withConnectTimeout(ASYNC_CONNECT_TIMEOUT).create() .calculateMass("jake", callback); control.verify(); } @Test public void testConnectionPoolTimeout() throws Exception { expectConnectionPoolTimeout(Config.DEFAULT_CONNECT_TIMEOUT); Thrift<TestService> thrift = createThrift(expectUnusedExecutorService()); control.replay(); TestService testService = thrift.builder().blocking().create(); try { testService.calculateMass("jake"); fail("Expected a TTimeoutException."); } catch (TTimeoutException e) { // Expected } control.verify(); } @Test public void testDoCallDeadlineNoThreads() throws Exception { control.replay(); ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>()); Thrift<TestService> thrift = createThrift(executorService); final TestService service = thrift.builder().noRetries().withRequestTimeout(Amount.of(1L, Time.SECONDS)).create(); final CountDownLatch remoteCallComplete = new CountDownLatch(1); final CountDownLatch remoteCallStarted = new CountDownLatch(1); Future<Integer> result = executorService.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { remoteCallStarted.countDown(); remoteCallComplete.await(); return service.calculateMass("jake"); } }); remoteCallStarted.await(); try { service.calculateMass("jake"); fail("Expected no available threads to trigger resource exhausted"); } catch (TResourceExhaustedException e) { // expected } finally { remoteCallComplete.countDown(); } try { result.get(); fail("Expected no available threads to trigger resource exhausted"); } catch (ExecutionException e) { assertEquals(TResourceExhaustedException.class, e.getCause().getClass()); } control.verify(); } private ExecutorService expectUnusedExecutorService() { return control.createMock(ExecutorService.class); } private static final String STAT_REQUESTS = "requests_events"; private static final String STAT_ERRORS = "errors"; private static final String STAT_RECONNECTS = "reconnects"; private static final String STAT_TIMEOUTS = "timeouts"; private void assertRequestsTotal(Thrift<?> thrift, int total) { assertRequestStatValue(STAT_REQUESTS, total); } private void assertErrorsTotal(Thrift<?> thrift, int total) { assertRequestStatValue(STAT_ERRORS, total); } private void assertReconnectsTotal(Thrift<?> thrift, int total) { assertRequestStatValue(STAT_RECONNECTS, total); } private void assertTimeoutsTotal(Thrift<?> thrift, int total) { assertRequestStatValue(STAT_TIMEOUTS, total); } private void assertRequestStatValue(String statName, long expectedValue) { Stat<Long> var = Stats.getVariable("foo_calculateMass_" + statName); assertNotNull(var); assertEquals(expectedValue, (long) var.read()); } private Thrift<TestService> createThrift(ExecutorService executorService) { return new Thrift<TestService>(executorService, connectionPool, requestTracker, "foo", TestService.class, clientFactory, false, false); } private Thrift<TestServiceAsync> createAsyncThrift(ExecutorService executorService) { return new Thrift<TestServiceAsync>(executorService, connectionPool, requestTracker, "foo", TestServiceAsync.class, asyncClientFactory, true, false); } private TestService expectServiceCall(boolean withFailure) throws ResourceExhaustedException, TimeoutException { Connection<TTransport, InetSocketAddress> connection = expectConnectionPoolGet(); return expectServiceCall(connection, withFailure); } private TestServiceAsync expectAsyncServiceCall(boolean withFailure) throws ResourceExhaustedException, TimeoutException { return expectAsyncServiceCall(expectConnectionPoolGet(ASYNC_CONNECT_TIMEOUT), withFailure); } private TestServiceAsync expectAsyncServiceRetry(boolean withFailure) throws ResourceExhaustedException, TimeoutException { return expectAsyncServiceCall( expectConnectionPoolGet(RetryingCaller.NONBLOCKING_TIMEOUT), withFailure); } private TestService expectThriftError(Capture<TTransport> transportCapture) throws ResourceExhaustedException, TimeoutException { Connection<TTransport, InetSocketAddress> connection = expectConnectionPoolGet(); return expectServiceCall(connection, transportCapture, true); } private Connection<TTransport, InetSocketAddress> expectConnectionPoolGet() throws ResourceExhaustedException, TimeoutException { Connection<TTransport, InetSocketAddress> connection = createConnection(); expect(connectionPool.get(Config.DEFAULT_CONNECT_TIMEOUT)).andReturn(connection); return connection; } private Connection<TTransport, InetSocketAddress> expectConnectionPoolGet( Amount<Long, Time> timeout) throws ResourceExhaustedException, TimeoutException { Connection<TTransport, InetSocketAddress> connection = createConnection(); expect(connectionPool.get(timeout)).andReturn(connection); return connection; } private void expectConnectionPoolResourceExhausted(Amount<Long, Time> timeout) throws ResourceExhaustedException, TimeoutException { expect(connectionPool.get(timeout)).andThrow(new ResourceExhaustedException("")); } private void expectConnectionPoolTimeout(Amount<Long, Time> timeout) throws ResourceExhaustedException, TimeoutException { expect(connectionPool.get(timeout)).andThrow(new TimeoutException()); } private Connection<TTransport, InetSocketAddress> createConnection() { return new TTransportConnection(new MockTSocket(), InetSocketAddress.createUnresolved(MockTSocket.HOST, MockTSocket.PORT)); } private TestService expectServiceCall(Connection<TTransport, InetSocketAddress> connection, boolean withFailure) { return expectServiceCall(connection, null, withFailure); } private TestServiceAsync expectAsyncServiceCall( Connection<TTransport, InetSocketAddress> connection, boolean withFailure) { return expectAsyncServiceCall(connection, null, withFailure); } private TestService expectServiceCall(Connection<TTransport, InetSocketAddress> connection, Capture<TTransport> transportCapture, boolean withFailure) { TestService testService = control.createMock(TestService.class); if (connection != null) { IExpectationSetters<TestService> expectApply = transportCapture == null ? expect(clientFactory.apply(EasyMock.isA(TTransport.class))) : expect(clientFactory.apply(EasyMock.capture(transportCapture))); expectApply.andReturn(testService); if (withFailure) { connectionPool.remove(connection); } else { connectionPool.release(connection); } } return testService; } private TestServiceAsync expectAsyncServiceCall( Connection<TTransport, InetSocketAddress> connection, Capture<TTransport> transportCapture, boolean withFailure) { TestServiceAsync testService = control.createMock(TestServiceAsync.class); if (connection != null) { IExpectationSetters<TestServiceAsync> expectApply = transportCapture == null ? expect(asyncClientFactory.apply(EasyMock.isA(TTransport.class))) : expect(asyncClientFactory.apply(EasyMock.capture(transportCapture))); expectApply.andReturn(testService); if (withFailure) { connectionPool.remove(connection); } else { connectionPool.release(connection); } } return testService; } }