// ================================================================================================= // 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.net.loadbalancing; import com.google.common.base.Function; import com.google.common.collect.Sets; import com.google.common.base.Predicate; import com.twitter.common.base.Closure; import com.twitter.common.net.pool.ResourceExhaustedException; import com.twitter.common.quantity.Amount; import com.twitter.common.quantity.Time; import com.twitter.common.testing.EasyMockTest; import com.twitter.common.net.loadbalancing.LoadBalancingStrategy.ConnectionResult; import com.twitter.common.net.loadbalancing.RequestTracker.RequestResult; import com.twitter.common.util.BackoffDecider; import com.twitter.common.util.Random; import com.twitter.common.util.TruncatedBinaryBackoff; import com.twitter.common.util.testing.FakeClock; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.util.Collection; import static org.easymock.EasyMock.expect; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; /** * @author William Farner */ public class MarkDeadStrategyTest extends EasyMockTest { private static final Amount<Long, Time> INITIAL_BACKOFF = Amount.of(1L, Time.SECONDS); private static final Amount<Long, Time> MAX_BACKOFF = Amount.of(10L, Time.SECONDS); private static final String BACKEND_1 = "backend1"; private static final String BACKEND_2 = "backend2"; private LoadBalancingStrategy<String> wrappedStrategy; private Closure<Collection<String>> onBackendsChosen; private Predicate<String> mockHostChecker; private LoadBalancingStrategy<String> markDead; private Random random; private FakeClock clock; @Before public void setUp() { wrappedStrategy = createMock(new Clazz<LoadBalancingStrategy<String>>() {}); onBackendsChosen = createMock(new Clazz<Closure<Collection<String>>>() {}); mockHostChecker = createMock(new Clazz<Predicate<String>>() {}); random = createMock(Random.class); clock = new FakeClock(); Function<String, BackoffDecider> backoffFactory = new Function<String, BackoffDecider>() { @Override public BackoffDecider apply(String s) { return BackoffDecider.builder(s) .withSeedSize(1) .withClock(clock) .withRandom(random) .withTolerateFailureRate(0.5) .withStrategy(new TruncatedBinaryBackoff(INITIAL_BACKOFF, MAX_BACKOFF)) // This recovery type is suggested for load balancer strategies to prevent // connection pool churn that would occur from the random linear recovery type. .withRecoveryType(BackoffDecider.RecoveryType.FULL_CAPACITY) .withRequestWindow(MAX_BACKOFF) .build(); } }; markDead = new MarkDeadStrategy<String>(wrappedStrategy, backoffFactory, mockHostChecker); } @After public void verify() { control.verify(); } @Test(expected = ResourceExhaustedException.class) public void testNoBackends() throws ResourceExhaustedException { expect(wrappedStrategy.nextBackend()).andThrow(new ResourceExhaustedException("No backends.")); control.replay(); markDead.nextBackend(); } @Test public void testForwardsBasicCalls() throws ResourceExhaustedException { wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expect(wrappedStrategy.nextBackend()).andReturn(BACKEND_1); control.replay(); markDead.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); assertThat(markDead.nextBackend(), is(BACKEND_1)); } @Test public void testAllHealthy() { wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 10); expectRequest(BACKEND_1, RequestResult.SUCCESS, 10); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 10); expectRequest(BACKEND_2, RequestResult.SUCCESS, 10); control.replay(); markDead.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); request(BACKEND_1, RequestResult.SUCCESS, connect(BACKEND_1, ConnectionResult.SUCCESS, 10)); request(BACKEND_2, RequestResult.SUCCESS, connect(BACKEND_2, ConnectionResult.SUCCESS, 10)); } @Test public void testOneFailingConnections() { wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 10); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 4); expectConnected(BACKEND_2, ConnectionResult.FAILED, 4); wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1), onBackendsChosen); control.replay(); markDead.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); connect(BACKEND_1, ConnectionResult.SUCCESS, 10); connect(BACKEND_2, ConnectionResult.SUCCESS, 4); connect(BACKEND_2, ConnectionResult.FAILED, 10); } @Test public void testOneFailingRequests() { wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 10); expectRequest(BACKEND_1, RequestResult.SUCCESS, 10); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 10); expectRequest(BACKEND_2, RequestResult.SUCCESS, 10); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 10); expectRequest(BACKEND_1, RequestResult.FAILED, 30); wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_2), onBackendsChosen); control.replay(); markDead.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); request(BACKEND_1, RequestResult.SUCCESS, connect(BACKEND_1, ConnectionResult.SUCCESS, 10)); request(BACKEND_2, RequestResult.SUCCESS, connect(BACKEND_2, ConnectionResult.SUCCESS, 10)); connect(BACKEND_1, ConnectionResult.SUCCESS, 10); request(BACKEND_1, RequestResult.FAILED, 50); } @Test public void testOneTimingOut() { wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 10); expectRequest(BACKEND_1, RequestResult.SUCCESS, 10); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 10); expectRequest(BACKEND_2, RequestResult.SUCCESS, 10); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 10); expectRequest(BACKEND_2, RequestResult.TIMEOUT, 30); wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1), onBackendsChosen); control.replay(); markDead.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); request(BACKEND_1, RequestResult.SUCCESS, connect(BACKEND_1, ConnectionResult.SUCCESS, 10)); request(BACKEND_2, RequestResult.SUCCESS, connect(BACKEND_2, ConnectionResult.SUCCESS, 10)); connect(BACKEND_2, ConnectionResult.SUCCESS, 10); request(BACKEND_2, RequestResult.TIMEOUT, 50); } @Test public void testFailingRecovers() { wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 10); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 4); expectConnected(BACKEND_2, ConnectionResult.FAILED, 4); wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1), onBackendsChosen); wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 10); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 9); expect(mockHostChecker.apply(BACKEND_2)).andReturn(true); control.replay(); markDead.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); connect(BACKEND_1, ConnectionResult.SUCCESS, 10); connect(BACKEND_2, ConnectionResult.SUCCESS, 4); connect(BACKEND_2, ConnectionResult.FAILED, 5); connect(BACKEND_1, ConnectionResult.SUCCESS, 5); clock.advance(INITIAL_BACKOFF); // Wait for backoff period to expire. clock.waitFor(1); clock.advance(INITIAL_BACKOFF); // Wait for recovery period to expire. connect(BACKEND_1, ConnectionResult.SUCCESS, 5); connect(BACKEND_2, ConnectionResult.SUCCESS, 9); } @Test public void testFailingServerWithLiveHostChecker() { wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 10); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 4); expectConnected(BACKEND_2, ConnectionResult.FAILED, 4); wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 10); expect(mockHostChecker.apply(BACKEND_2)).andReturn(false); wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 5); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 10); expect(mockHostChecker.apply(BACKEND_2)).andReturn(true); control.replay(); markDead.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); connect(BACKEND_1, ConnectionResult.SUCCESS, 10); connect(BACKEND_2, ConnectionResult.SUCCESS, 4); connect(BACKEND_2, ConnectionResult.FAILED, 5); connect(BACKEND_1, ConnectionResult.SUCCESS, 5); clock.advance(INITIAL_BACKOFF); // Wait for backoff period to expire. clock.waitFor(1); clock.advance(INITIAL_BACKOFF); // Wait for recovery period to expire. connect(BACKEND_1, ConnectionResult.SUCCESS, 5); clock.advance(INITIAL_BACKOFF); // Wait for backoff period to expire. clock.waitFor(1); clock.advance(INITIAL_BACKOFF); // Wait for recovery period to expire. connect(BACKEND_1, ConnectionResult.SUCCESS, 5); connect(BACKEND_2, ConnectionResult.SUCCESS, 10); } @Test public void testAllDead() { wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 10); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 10); expectConnected(BACKEND_1, ConnectionResult.FAILED, 10); wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_2), onBackendsChosen); expectConnected(BACKEND_2, ConnectionResult.FAILED, 10); wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_2, ConnectionResult.FAILED, 5); control.replay(); markDead.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); connect(BACKEND_1, ConnectionResult.SUCCESS, 10); connect(BACKEND_2, ConnectionResult.SUCCESS, 10); connect(BACKEND_1, ConnectionResult.FAILED, 15); connect(BACKEND_2, ConnectionResult.FAILED, 15); } @Test public void testRecoversFromForcedLiveMode() { wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_1, ConnectionResult.SUCCESS, 5); expectConnected(BACKEND_1, ConnectionResult.FAILED, 5); // Backend 1 starts backing off. wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_2), onBackendsChosen); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 5); expectConnected(BACKEND_2, ConnectionResult.FAILED, 5); // Backend 2 starts backing off. wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); expectConnected(BACKEND_2, ConnectionResult.SUCCESS, 5); expectConnected(BACKEND_2, ConnectionResult.FAILED, 5); // Backend 2 starts backing off. wrappedStrategy.offerBackends(Sets.newHashSet(BACKEND_1), onBackendsChosen); control.replay(); markDead.offerBackends(Sets.newHashSet(BACKEND_1, BACKEND_2), onBackendsChosen); connect(BACKEND_1, ConnectionResult.SUCCESS, 5); connect(BACKEND_1, ConnectionResult.FAILED, 6); // BACKEND_1 gets marked as dead. connect(BACKEND_2, ConnectionResult.SUCCESS, 5); connect(BACKEND_2, ConnectionResult.FAILED, 6); // All now marked dead, forced into live mode. clock.advance(INITIAL_BACKOFF); // Wait for backoff period to expire. clock.waitFor(1); connect(BACKEND_2, ConnectionResult.SUCCESS, 5); connect(BACKEND_2, ConnectionResult.FAILED, 5); // BACKEND_2 marked as dead. } private int connect(String backend, ConnectionResult result, int count) { for (int i = 0; i < count; i++) { markDead.addConnectResult(backend, result, 0L); } return count; } private void request(String backend, RequestResult result, int count) { for (int i = 0; i < count; i++) { markDead.addRequestResult(backend, result, 0L); } } private void expectConnected(String backend, ConnectionResult result, int count) { for (int i = 0; i < count; i++) { wrappedStrategy.addConnectResult(backend, result, 0L); } } private void expectRequest(String backend, RequestResult result, int count) { for (int i = 0; i < count; i++) { wrappedStrategy.addRequestResult(backend, result, 0L); } } }