// =================================================================================================
// 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.util;
import com.google.common.collect.Sets;
import com.twitter.common.quantity.Amount;
import com.twitter.common.quantity.Time;
import com.twitter.common.util.testing.FakeClock;
import org.easymock.IMocksControl;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import static org.easymock.EasyMock.createControl;
import static org.easymock.EasyMock.expect;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
/**
* @author William Farner
*/
public class BackoffDeciderTest {
private static final String NAME = "test_decider";
private IMocksControl control;
private FakeClock clock;
private Random random;
@Before
public void setUp() {
control = createControl();
random = control.createMock(Random.class);
clock = new FakeClock();
}
private BackoffDecider.Builder builder(String name) {
return new BackoffDecider.Builder(name)
.withSeedSize(1)
.withRequestWindow(Amount.of(10L, Time.SECONDS))
.withBucketCount(100)
.withClock(clock)
.withRandom(random);
}
@After
public void verify() {
control.verify();
}
@Test
public void testAllSuccess() {
control.replay();
BackoffDecider decider = builder(NAME).build();
run(decider, 10, Result.SUCCESS, State.NORMAL);
}
@Test
public void testAllFailures() {
control.replay();
BackoffDecider decider = builder(NAME).build();
run(decider, 10, Result.FAILURE, State.BACKOFF);
}
@Test
public void testSingleFailure() {
control.replay();
BackoffDecider decider = builder(NAME).build();
run(decider, 10, Result.SUCCESS, State.NORMAL);
run(decider, 1, Result.FAILURE, State.NORMAL);
}
@Test
public void testBelowThreshold() {
control.replay();
BackoffDecider decider = builder(NAME).withTolerateFailureRate(0.5).build();
run(decider, 5, Result.SUCCESS, State.NORMAL);
run(decider, 5, Result.FAILURE, State.NORMAL);
}
@Test
public void testAtThreshold() {
control.replay();
BackoffDecider decider = builder(NAME).withTolerateFailureRate(0.49).build();
run(decider, 51, Result.SUCCESS, State.NORMAL);
run(decider, 49, Result.FAILURE, State.NORMAL);
}
@Test
public void testAboveThreshold() {
control.replay();
BackoffDecider decider = builder(NAME).withTolerateFailureRate(0.49).build();
run(decider, 51, Result.SUCCESS, State.NORMAL);
run(decider, 49, Result.FAILURE, State.NORMAL);
run(decider, 1, Result.FAILURE, State.BACKOFF);
}
@Test
public void testRecoversFromBackoff() {
// Backoff for the single request during the recovery period.
expect(random.nextDouble()).andReturn(1d);
control.replay();
BackoffDecider decider = builder(NAME).build();
decider.addFailure();
assertThat(decider.shouldBackOff(), is(true));
// Enter recovery period.
clock.waitFor(101);
assertThat(decider.shouldBackOff(), is(true));
// Enter normal period.
clock.waitFor(101);
assertThat(decider.shouldBackOff(), is(false));
}
@Test
public void testLinearRecovery() {
for (int i = 0; i < 10; i++) {
expect(random.nextDouble()).andReturn(0.1 * i + 0.01); // Above threshold - back off.
expect(random.nextDouble()).andReturn(0.1 * i - 0.01); // Below threshold - allow request.
}
control.replay();
BackoffDecider decider = builder(NAME).build();
decider.addFailure(); // Moves into backoff state.
assertThat(decider.shouldBackOff(), is(true));
// Enter recovery period.
clock.waitFor(101);
// Step linearly through recovery period (100 ms).
for (int i = 0; i < 10; i++) {
clock.waitFor(10);
assertThat(decider.shouldBackOff(), is(true));
assertThat(decider.shouldBackOff(), is(false));
}
}
@Test
public void testExponentialBackoff() {
// Don't back off during recovery period.
expect(random.nextDouble()).andReturn(0d).atLeastOnce();
control.replay();
BackoffDecider decider = builder(NAME).build();
List<Integer> backoffDurationsMs = Arrays.asList(
100, 200, 400, 800, 1600, 3200, 6400, 10000, 10000);
assertThat(decider.shouldBackOff(), is(false));
// normal -> backoff
decider.addFailure();
assertThat(decider.shouldBackOff(), is(true));
for (int backoffDurationMs : backoffDurationsMs) {
assertThat(decider.shouldBackOff(), is(true));
// backoff -> recovery
clock.waitFor(backoffDurationMs + 1);
assertThat(decider.shouldBackOff(), is(false));
// recovery -> backoff
decider.addFailure();
}
}
@Test
public void testRequestsExpire() {
control.replay();
BackoffDecider decider = builder(NAME).build();
run(decider, 10, Result.SUCCESS, State.NORMAL);
run(decider, 10, Result.FAILURE, State.NORMAL);
assertThat(decider.shouldBackOff(), is(false));
// Depends on request window of 10 seconds, with 100 buckets.
clock.waitFor(10000);
run(decider, 1, Result.SUCCESS, State.NORMAL);
assertThat(decider.shouldBackOff(), is(false));
assertThat(decider.requests.totalRequests, is(21L));
assertThat(decider.requests.totalFailures, is(10L));
// Requests should have decayed out of the window.
clock.waitFor(101);
run(decider, 1, Result.SUCCESS, State.NORMAL);
assertThat(decider.shouldBackOff(), is(false));
assertThat(decider.requests.totalRequests, is(2L));
assertThat(decider.requests.totalFailures, is(0L));
}
@Test
public void testAllBackendsDontBackoff() {
// Back off for all requests during recovery period.
expect(random.nextDouble()).andReturn(1d); // decider2 in recovery.
expect(random.nextDouble()).andReturn(0d); // decider3 in recovery.
control.replay();
Set<BackoffDecider> group = Sets.newHashSet();
BackoffDecider decider1 = builder(NAME + 1).groupWith(group).build();
BackoffDecider decider2 = builder(NAME + 2).groupWith(group).build();
BackoffDecider decider3 = builder(NAME + 3).groupWith(group).build();
// Two of three backends start backing off.
decider1.addFailure();
assertThat(decider1.shouldBackOff(), is(true));
decider2.addFailure();
assertThat(decider2.shouldBackOff(), is(true));
// Since all but 1 backend is backing off, we switch out of backoff mode.
assertThat(decider3.shouldBackOff(), is(false));
decider3.addFailure();
assertThat(decider1.shouldBackOff(), is(false));
assertThat(decider2.shouldBackOff(), is(false));
assertThat(decider3.shouldBackOff(), is(false));
// Begin recovering one backend, others will return to recovery.
decider1.addSuccess();
assertThat(decider1.shouldBackOff(), is(false)); // Still thinks others are backing off.
assertThat(decider2.shouldBackOff(), is(false)); // Realizes decider1 is up, moves to recovery.
assertThat(decider2.shouldBackOff(), is(true)); // In recovery.
assertThat(decider3.shouldBackOff(), is(false)); // Realizes 1 & 2 are up, moves to recovery.
assertThat(decider3.shouldBackOff(), is(false)); // In recovery.
}
@Test
public void testOneBackendDoesntAffectOthers() {
control.replay();
Set<BackoffDecider> group = Sets.newHashSet();
BackoffDecider decider1 = builder(NAME + 1).groupWith(group).build();
BackoffDecider decider2 = builder(NAME + 2).groupWith(group).build();
BackoffDecider decider3 = builder(NAME + 3).groupWith(group).build();
// One backend starts failing.
run(decider1, 10, Result.SUCCESS, State.NORMAL);
run(decider2, 10, Result.SUCCESS, State.NORMAL);
run(decider3, 10, Result.FAILURE, State.BACKOFF);
// Other backends should remain normal.
run(decider1, 10, Result.SUCCESS, State.NORMAL);
run(decider2, 10, Result.SUCCESS, State.NORMAL);
}
@Test
public void testPreventsBackoffFlapping() {
// Permit requests during the backoff period.
expect(random.nextDouble()).andReturn(0d).atLeastOnce();
control.replay();
BackoffDecider decider = builder(NAME).build();
// Simulate 20 threads being permitted to send a request.
for (int i = 0; i < 20; i++) assertThat(decider.shouldBackOff(), is(false));
// The first 4 threads succeed.
for (int i = 0; i < 4; i++) decider.addSuccess();
assertThat(decider.shouldBackOff(), is(false));
// The next 6 fail, triggering backoff mode.
for (int i = 0; i < 6; i++) decider.addFailure();
assertThat(decider.shouldBackOff(), is(true));
// The next 10 succeed, but we are already backing off...so we should not move out of backoff.
for (int i = 0; i < 10; i++) decider.addSuccess();
assertThat(decider.shouldBackOff(), is(true));
// Attempt to push the decider into a higher backoff period.
for (int i = 0; i < 10; i++) decider.addFailure();
// Verify that the initial backoff period is in effect.
clock.waitFor(101);
assertThat(decider.shouldBackOff(), is(false));
}
private enum Result {
SUCCESS, FAILURE
}
private enum State {
BACKOFF, NORMAL
}
private void run(BackoffDecider decider, int numRequests, Result result, State state) {
for (int i = 0; i < numRequests; i++) {
if (result == Result.SUCCESS) {
decider.addSuccess();
} else {
decider.addFailure();
}
boolean backingOff = state == State.BACKOFF;
assertThat(decider.shouldBackOff(), is(backingOff));
}
}
}