package com.linkedin.d2.balancer.strategies.degrader;
import com.linkedin.d2.balancer.clients.TrackerClient;
import com.linkedin.d2.balancer.strategies.degrader.DegraderLoadBalancerStrategyV3.DegraderLoadBalancerState;
import com.linkedin.d2.balancer.strategies.degrader.DegraderLoadBalancerStrategyV3.PartitionDegraderLoadBalancerState;
import com.linkedin.util.clock.SettableClock;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.testng.annotations.Test;
import static com.linkedin.d2.balancer.util.TestHelper.assertSameElements;
import static com.linkedin.d2.balancer.util.TestHelper.concurrently;
import static com.linkedin.d2.balancer.util.TestHelper.getAll;
import static com.linkedin.d2.balancer.util.TestHelper.split;
import static org.testng.Assert.*;
public class DegraderLoadBalancerStateTest
{
private static final String SERVICE_NAME = "test";
/**
* Resizing the array of partitions doesn't interfere with setting partition state.
*/
@Test(groups = {"small", "back-end"})
public void testConcurrentResizeAndSet()
throws InterruptedException
{
// This test aims to reproduce a specific bug, which occurs when one thread sets a
// partition state while another thread is in the middle of resizing the array of states.
// To reproduce this, we inject a tricky Clock, which pauses execution of the latter
// thread in the middle of resizing (when constructing the new partition state).
// This depends on DegraderLoadBalancerState to call the clock at least once to initialize
// partition 1. If that changes, you'll have to change clock-related constants below.
final PauseClock clock = new PauseClock();
final DegraderLoadBalancerState subject
= new DegraderLoadBalancerStrategyV3
(new DegraderLoadBalancerStrategyConfig(5000, true, 1, null, Collections.<String, Object>emptyMap(),
clock, 1, 1, 1, 1, 1, 1, 1, 1, 0.2, null, 21, null,
0.1, null, null, null, null, 100),
SERVICE_NAME, null).getState();
Thread getPartition1 = new Thread()
{
@Override
public void run()
{
subject.getPartitionState(1); // resize the array as a side-effect
}
};
assertNotNull(subject.getPartitionState(0));
final long clockCalled = clock._calls.get();
assertTrue(clockCalled > 0, "clock not called"); // 1 partition initialized (so far)
clock._paused = new CountDownLatch(1);
clock._resume = new CountDownLatch(1);
getPartition1.start();
assertTrue(clock._paused.await(60, TimeUnit.SECONDS));
// Now getPartition1 has started resizing the array.
final PartitionDegraderLoadBalancerState newState = newPartitionState(0, 0);
assertNotSame(subject.getPartitionState(0), newState);
subject.setPartitionState(0, newState);
assertSame(subject.getPartitionState(0), newState);
clock._resume.countDown();
getPartition1.join(60000);
assertFalse(getPartition1.isAlive());
// Now getPartition1 has finished resizing the array.
assertSame(subject.getPartitionState(0), newState); // as before
assertTrue(clock._calls.get() > clockCalled, "clock not called again"); // 2 partitions initialized
}
/**
* A clock that can pause execution of calls to currentTimeMillis.
*/
private static class PauseClock extends SettableClock
{
final AtomicLong _calls = new AtomicLong(0);
CountDownLatch _paused = new CountDownLatch(0);
CountDownLatch _resume = new CountDownLatch(0);
@Override
public long currentTimeMillis()
{
_calls.incrementAndGet();
_paused.countDown();
try
{
_resume.await();
}
catch (Exception e)
{
fail(e + "", e);
}
return super.currentTimeMillis();
}
}
private static PartitionDegraderLoadBalancerState newPartitionState(long generationID, long lastUpdated)
{
return new PartitionDegraderLoadBalancerState(generationID, lastUpdated,
false, new DegraderRingFactory<>(new DegraderLoadBalancerStrategyConfig(1L)),
Collections.<URI, Integer>emptyMap(),
PartitionDegraderLoadBalancerState.Strategy.LOAD_BALANCE,
0, 0, Collections.<TrackerClient, Double>emptyMap(),
SERVICE_NAME, Collections.<String, String>emptyMap(), 0,
Collections.emptyMap(), Collections.emptyMap());
}
private static List<PartitionDegraderLoadBalancerState> newPartitionStates(int numberOfPartitions)
{
List<PartitionDegraderLoadBalancerState> states = new ArrayList<PartitionDegraderLoadBalancerState>();
for (int p = 0; p < numberOfPartitions; ++p)
states.add(newPartitionState(p, p));
return states;
}
/**
* Concurrent calls to getPartitionState don't interfere with each other.
* This test isn't repeatable, since the timing of the threads' execution is unpredictable.
*/
@Test(groups = {"small", "back-end"})
public void testConcurrentGets()
{
testConcurrentGets(8);
testConcurrentGets(11);
}
private static void testConcurrentGets(int numberOfPartitions)
{
DegraderLoadBalancerState subject = DegraderLoadBalancerTest.getStrategy().getState();
List<PartitionDegraderLoadBalancerState> a1 = concurrentGets(subject, numberOfPartitions);
List<PartitionDegraderLoadBalancerState> a2 = concurrentGets(subject, (numberOfPartitions * 2) + 1);
assertSameElements(a1, a2.subList(0, a1.size()));
}
/**
* Call subject.getPartitionState(i) concurrently, for i in 0 .. numberOfPartitions-1.
* Run several threads for each partition; make all the threads call subject concurrently.
*
* @return [subject.getPartitionState(0) .. subject.getPartitionState(numberOfPartitions - 1)]
*/
private static List<PartitionDegraderLoadBalancerState> concurrentGets(DegraderLoadBalancerState subject,
int numberOfPartitions)
{
int getsPerPartition = 3;
List<Callable<PartitionDegraderLoadBalancerState>> reads
= new ArrayList<Callable<PartitionDegraderLoadBalancerState>>();
for (int g = 0; g < getsPerPartition; ++g)
for (int p = 0; p < numberOfPartitions; ++p)
reads.add(new GetPartitionState(subject, p));
List<List<PartitionDegraderLoadBalancerState>> actual =
split(getAll(concurrently(reads)), numberOfPartitions);
assertEquals(actual.size(), getsPerPartition);
List<PartitionDegraderLoadBalancerState> a0 = actual.get(0);
assertEquals(a0.size(), numberOfPartitions);
for (int a = 1; a < actual.size(); ++a)
assertSameElements(actual.get(a), a0);
return a0;
}
/**
* Concurrent calls to getPartitionState and setPartitionState don't interfere with each other.
* This test isn't repeatable, since the timing of the threads' execution is unpredictable.
*/
@Test(groups = {"small", "back-end"})
public void testConcurrentGetsAndSets()
{
testConcurrentGetsAndSets(8);
testConcurrentGetsAndSets(11);
}
private static void testConcurrentGetsAndSets(int numberOfPartitions)
{
DegraderLoadBalancerState subject = DegraderLoadBalancerTest.getStrategy().getState();
List<PartitionDegraderLoadBalancerState> newStates = newPartitionStates((numberOfPartitions * 2) + 1);
List<PartitionDegraderLoadBalancerState> a1 = concurrentGetsAndSets(subject, newStates.subList(0, numberOfPartitions));
List<PartitionDegraderLoadBalancerState> a2 = concurrentGetsAndSets(subject, newStates);
assertSameElements(a1, a2.subList(0, a1.size()));
}
/**
* Call subject.getPartitionState(i) and setPartitionState(i) concurrently, for i in 0 .. numberOfPartitions-1.
* Run several threads for each partition; make all the threads call subject concurrently.
*
* @return [subject.getPartitionState(0) .. subject.getPartitionState(numberOfPartitions - 1)]
*/
private static List<PartitionDegraderLoadBalancerState> concurrentGetsAndSets
(DegraderLoadBalancerState subject, List<PartitionDegraderLoadBalancerState> newStates)
{
int numberOfPartitions = newStates.size();
int getsPerPartition = 3;
List<Callable<PartitionDegraderLoadBalancerState>> calls = new ArrayList<Callable<PartitionDegraderLoadBalancerState>>();
for (int p = 0; p < numberOfPartitions; ++p)
calls.add(new GetAndSetPartitionState(subject, p, newStates.get(p)));
for (int g = 0; g < getsPerPartition; ++g)
for (int p = 0; p < numberOfPartitions; ++p)
calls.add(new GetPartitionState(subject, p));
getAll(concurrently(calls));
List<PartitionDegraderLoadBalancerState> actual = new ArrayList<PartitionDegraderLoadBalancerState>();
for (int p = 0; p < numberOfPartitions; ++p)
actual.add(subject.getPartitionState(p));
assertSameElements(actual, newStates);
return actual;
}
/**
* Call DegraderLoadBalancerState.getPartitionState.
*/
private static class GetPartitionState implements Callable<PartitionDegraderLoadBalancerState>
{
private final DegraderLoadBalancerState _state;
private final int _partitionID;
GetPartitionState(DegraderLoadBalancerState state, int partitionID)
{
_state = state;
_partitionID = partitionID;
}
@Override
public PartitionDegraderLoadBalancerState call()
throws InterruptedException
{
return _state.getPartitionState(_partitionID);
}
}
/**
* Call DegraderLoadBalancerState.getPartitionState and then setPartitionState.
*/
private static class GetAndSetPartitionState implements Callable<PartitionDegraderLoadBalancerState>
{
private final DegraderLoadBalancerState _state;
private final int _partitionID;
private final PartitionDegraderLoadBalancerState _newPartitionState;
GetAndSetPartitionState(DegraderLoadBalancerState state,
int partitionID,
PartitionDegraderLoadBalancerState newPartitionState)
{
_state = state;
_partitionID = partitionID;
_newPartitionState = newPartitionState;
}
@Override
public PartitionDegraderLoadBalancerState call()
{
try
{
return _state.getPartitionState(_partitionID);
}
finally
{
_state.setPartitionState(_partitionID, _newPartitionState);
}
}
}
}