/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License 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.
*/
package com.github.ambry.router;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.github.ambry.clustermap.MockDataNodeId;
import com.github.ambry.clustermap.MockPartitionId;
import com.github.ambry.clustermap.MockReplicaId;
import com.github.ambry.clustermap.ReplicaId;
import com.github.ambry.network.Port;
import com.github.ambry.network.PortType;
import com.github.ambry.utils.MockTime;
import com.github.ambry.utils.Time;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import static org.junit.Assert.*;
/**
* Unit test for the following operation trackers
* 1. {@link SimpleOperationTracker}
* 2. {@link AdaptiveOperationTracker}
*
* The status of an operation is represented as in the following format:
*
* local unsent count-local inflight count-local succeeded count-local failed count;
* remote unsent count-remote inflight count-remote succeeded count-remote failed count
*
* For example: 3-0-0-0; 9-0-0-0
*
* The number of replicas for most tests is 12.
*/
@RunWith(Parameterized.class)
public class OperationTrackerTest {
private static final int PORT = 6666;
private static final String SIMPLE_OP_TRACKER = "Simple";
private static final String ADAPTIVE_OP_TRACKER = "Adaptive";
private static final double QUANTILE = 0.9;
private final String operationTrackerType;
private List<MockDataNodeId> datanodes;
private MockPartitionId mockPartition;
private String localDcName;
private final LinkedList<ReplicaId> inflightReplicas = new LinkedList<>();
private final Set<ReplicaId> repetitionTracker = new HashSet<>();
// for AdaptiveOperationTracker
private final Time time = new MockTime();
private final MetricRegistry registry = new MetricRegistry();
private final Histogram localColoTracker = registry.histogram("LocalColoTracker");
private final Histogram crossColoTracker = registry.histogram("CrossColoTracker");
private final Counter pastDueCounter = registry.counter("PastDueCounter");
/**
* Running for both {@link SimpleOperationTracker} and {@link AdaptiveOperationTracker}
* @return an array with both {@link #SIMPLE_OP_TRACKER} and {@link #ADAPTIVE_OP_TRACKER}
*/
@Parameterized.Parameters
public static List<Object[]> data() {
return Arrays.asList(new Object[][]{{SIMPLE_OP_TRACKER}, {ADAPTIVE_OP_TRACKER}});
}
/**
* @param operationTrackerType the type of {@link OperationTracker} that needs to be used in tests
*/
public OperationTrackerTest(String operationTrackerType) {
this.operationTrackerType = operationTrackerType;
}
/**
* crossColoEnabled = false, successTarget = 2, parallelism = 3.
*
* <p/>
* 1. Get 3 local replicas to send request (and send requests);
* 2. 2 replicas succeeds.
* 3. Operation succeeds.
* 4. 1 local fails.
* 5. Operation remains succeeded.
*/
@Test
public void localSucceedTest() {
initialize();
OperationTracker ot = getOperationTracker(false, 2, 3);
// 3-0-0-0; 9-0-0-0
assertFalse("Operation should not have been done.", ot.isDone());
sendRequests(ot, 3, false);
// 0-3-0-0; 9-0-0-0
assertFalse("Operation should not have been done.", ot.isDone());
for (int i = 0; i < 2; i++) {
ot.onResponse(inflightReplicas.poll(), true);
}
// 0-1-2-0; 9-0-0-0
assertTrue("Operation should have succeeded", ot.hasSucceeded());
assertTrue("Operation should be done", ot.isDone());
ot.onResponse(inflightReplicas.poll(), false);
// 0-0-2-1; 9-0-0-0
assertTrue("Operation should have succeeded", ot.hasSucceeded());
assertTrue("Operation should be done", ot.isDone());
}
/**
* crossColoEnabled = false, successTarget = 2, parallelism = 3.
*
* <p/>
* 1. Get 3 local replicas to send request (and send requests);
* 2. 1 local replicas succeeded, 2 failed.
* 3. Operation fails.
*/
@Test
public void localFailTest() {
initialize();
OperationTracker ot = getOperationTracker(false, 2, 3);
// 3-0-0-0; 9-0-0-0
assertFalse("Operation should not have been done.", ot.isDone());
sendRequests(ot, 3, false);
// 0-3-0-0; 9-0-0-0
for (int i = 0; i < 2; i++) {
ot.onResponse(inflightReplicas.poll(), false);
}
assertFalse("Operation should not have succeeded", ot.hasSucceeded());
assertTrue("Operation should be done", ot.isDone());
// 0-1-0-2; 9-0-0-0
ot.onResponse(inflightReplicas.poll(), true);
// 0-0-1-2; 9-0-0-0
assertFalse("Operation should not have succeeded", ot.hasSucceeded());
assertTrue("Operation should be done", ot.isDone());
}
/**
* crossColoEnabled = true, successTarget = 1, parallelism = 2.
* <p/>
* 1. Get 2 local replicas to send request (and send requests);
* 2. 1 fails, 1 pending.
* 3. Get 1 more local replicas to send request (and send requests);
* 4. 1 succeeds.
* 5. Operation succeeds.
*/
@Test
public void localSucceedWithDifferentParameterTest() {
initialize();
OperationTracker ot = getOperationTracker(true, 1, 2);
// 3-0-0-0; 9-0-0-0
sendRequests(ot, 2, false);
// 1-2-0-0; 9-0-0-0
ot.onResponse(inflightReplicas.poll(), false);
// 1-1-0-1; 9-0-0-0
assertFalse("Operation should not have been done.", ot.isDone());
sendRequests(ot, 1, false);
// 0-2-0-1; 9-0-0-0
assertFalse("Operation should not be done", ot.isDone());
ot.onResponse(inflightReplicas.poll(), true);
// 0-1-1-1; 9-0-0-0
assertTrue("Operation should have succeeded", ot.hasSucceeded());
assertTrue("Operation should be done", ot.isDone());
}
/**
* crossColoEnabled = true, successTarget = 1, parallelism = 2.
* <p/>
* 1. Get 2 local replicas to send request (and send requests);
* 2. 1 local replica fails, 1 pending.
* 3. Get 1 more local replicas to send request (and send requests);
* 4. 2 local replica fails.
* 5. Get 1 remote replica from each Dc to send request (and send requests);
* 6. All fails.
* 7. Get 1 remote replica from each DC to send request (and send requests);
* 8. 1 fails, 2 pending.
* 9. Get 1 remote replica from each DC to send request (and send requests);
* 10. 2 fails.
* 11. 1 succeeds.
* 12. Operation succeeds.
*/
@Test
public void remoteReplicaTest() {
initialize();
OperationTracker ot = getOperationTracker(true, 1, 2);
// 3-0-0-0; 9-0-0-0
sendRequests(ot, 2, false);
// 1-2-0-0; 9-0-0-0
ReplicaId id = inflightReplicas.poll();
assertEquals("First request should have been to local DC", localDcName, id.getDataNodeId().getDatacenterName());
ot.onResponse(id, false);
// 1-1-0-1; 9-0-0-0
assertFalse("Operation should not be done", ot.isDone());
sendRequests(ot, 1, false);
// 0-2-0-1; 9-0-0-0
id = inflightReplicas.poll();
assertEquals("Second request should have been to local DC", localDcName, id.getDataNodeId().getDatacenterName());
ot.onResponse(id, false);
id = inflightReplicas.poll();
assertEquals("Third request should have been to local DC", localDcName, id.getDataNodeId().getDatacenterName());
ot.onResponse(id, false);
// 0-0-0-3; 9-0-0-0
assertFalse("Operation should not be done", ot.isDone());
sendRequests(ot, 2, false);
// 0-0-0-3; 7-2-0-0
assertFalse("Operation should not be done", ot.isDone());
for (int i = 0; i < 2; i++) {
ot.onResponse(inflightReplicas.poll(), false);
}
// 0-0-0-3; 7-0-0-2
assertFalse("Operation should not be done", ot.isDone());
sendRequests(ot, 2, false);
// 0-0-0-3; 5-2-0-2
ot.onResponse(inflightReplicas.poll(), false);
assertFalse("Operation should not be done", ot.isDone());
// 0-0-0-3; 5-1-0-3
sendRequests(ot, 1, false);
// 0-0-0-3; 4-1-0-3
ot.onResponse(inflightReplicas.poll(), true);
assertTrue("Operation should have succeeded", ot.hasSucceeded());
assertTrue("Operation should be done", ot.isDone());
}
/**
* crossColoEnabled = true, successTarget = 12, parallelism = 3.
*
* This test may be meaningful for DELETE operation.
*
* <p/>
* 1. Get 3 local replicas to send request (and send requests);
* 2. 3 succeeded.
* 3. Operation succeeded.
*/
@Test
public void fullSuccessTargetTest() {
initialize();
OperationTracker ot = getOperationTracker(true, 12, 3);
while (!ot.hasSucceeded()) {
sendRequests(ot, 3, false);
for (int i = 0; i < 3; i++) {
ot.onResponse(inflightReplicas.poll(), true);
}
}
assertTrue("Operation should have succeeded", ot.hasSucceeded());
assertTrue("Operation should be done", ot.isDone());
}
/**
* crossColoEnabled = true, successTarget = 1, parallelism = 2.
* Only 4 local replicas
*
* 1. Get 1st local replica to send request (and sent);
* 2. Get 2nd local replica to send request (and failed to send);
* 3. Get 3rd local replica to send request (and sent);
* 4. Receive 2 failed responses from the 1st and 3rd replicas;
* 5. Get again 2nd local replica to send request (and sent);
* 6. Get 4th local replica to send request (and failed to send);
* 7. Receive 1 failed responses from the 2nd replicas;
* 8. Get again 4th local replica to send request (and sent);
* 9. Receive 1 successful response from the 4th replica;
* 10. Operation succeeds.
*/
@Test
public void useReplicaNotSucceededSendTest() {
int replicaCount = 4;
List<Port> portList = Collections.singletonList(new Port(PORT, PortType.PLAINTEXT));
List<String> mountPaths = Collections.singletonList("mockMountPath");
datanodes = Collections.singletonList(new MockDataNodeId(portList, mountPaths, "dc-0"));
mockPartition = new MockPartitionId();
populateReplicaList(replicaCount);
localDcName = datanodes.get(0).getDatacenterName();
OperationTracker ot = getOperationTracker(true, 1, 2);
sendRequests(ot, 2, true);
ot.onResponse(inflightReplicas.poll(), false);
ot.onResponse(inflightReplicas.poll(), false);
assertFalse("Operation should not be done", ot.isDone());
sendRequests(ot, 1, true);
ot.onResponse(inflightReplicas.poll(), false);
assertFalse("Operation should not be done", ot.isDone());
sendRequests(ot, 1, false);
ot.onResponse(inflightReplicas.poll(), true);
assertTrue("Operation should have succeeded", ot.hasSucceeded());
assertTrue("Operation should be done", ot.isDone());
}
/**
* Test to ensure that replicas that are down are also returned by the operation tracker, but they are
* ordered after the healthy replicas.
*/
@Test
public void downReplicasOrderingTest() {
List<Port> portList = Collections.singletonList(new Port(PORT, PortType.PLAINTEXT));
List<String> mountPaths = Collections.singletonList("mockMountPath");
datanodes = new ArrayList<>();
datanodes.add(new MockDataNodeId(portList, mountPaths, "dc-0"));
datanodes.add(new MockDataNodeId(portList, mountPaths, "dc-1"));
mockPartition = new MockPartitionId();
int replicaCount = 6;
populateReplicaList(replicaCount);
// Test scenarios with various number of replicas down
for (int i = 0; i < replicaCount; i++) {
testReplicaDown(replicaCount, i);
}
}
/**
* Tests the case when the success target > number of replicas.
*/
@Test
public void notEnoughReplicasToMeetTargetTest() {
initialize();
try {
getOperationTracker(true, 13, 3);
fail("Should have failed to construct tracker because success target > replica count");
} catch (IllegalArgumentException e) {
// expected. Nothing to do.
}
}
/**
* Tests the case when parallelism < 1
*/
@Test
public void incorrectParallelismTest() {
initialize();
for (int parallelism : Arrays.asList(0, -1)) {
try {
getOperationTracker(true, 13, 0);
fail("Should have failed to construct tracker because parallelism is " + parallelism);
} catch (IllegalArgumentException e) {
// expected. Nothing to do.
}
}
}
/**
* Initialize 4 DCs, each DC has 1 data node, which has 3 replicas.
*/
private void initialize() {
int replicaCount = 12;
List<Port> portList = Collections.singletonList(new Port(PORT, PortType.PLAINTEXT));
List<String> mountPaths = Collections.singletonList("mockMountPath");
datanodes = new ArrayList<>(Arrays.asList(
new MockDataNodeId[]{new MockDataNodeId(portList, mountPaths, "dc-0"), new MockDataNodeId(portList, mountPaths,
"dc-1"), new MockDataNodeId(portList, mountPaths, "dc-2"), new MockDataNodeId(portList, mountPaths,
"dc-3")}));
mockPartition = new MockPartitionId();
populateReplicaList(replicaCount);
localDcName = datanodes.get(0).getDatacenterName();
}
/**
* Populate replicas for a partition..
* @param replicaCount The number of replicas to populate.
*/
private void populateReplicaList(int replicaCount) {
for (int i = 0; i < replicaCount; i++) {
mockPartition.replicaIds.add(new MockReplicaId(PORT, mockPartition, datanodes.get(i % datanodes.size()), 0));
}
}
/**
* Returns the right {@link OperationTracker} based on {@link #operationTrackerType}.
* @param crossColoEnabled {@code true} if cross colo needs to be enabled. {@code false} otherwise.
* @param successTarget the number of successful responses required for the operation to succeed.
* @param parallelism the number of parallel requests that can be in flight.
* @return the right {@link OperationTracker} based on {@link #operationTrackerType}.
*/
private OperationTracker getOperationTracker(boolean crossColoEnabled, int successTarget, int parallelism) {
OperationTracker tracker;
switch (operationTrackerType) {
case SIMPLE_OP_TRACKER:
tracker = new SimpleOperationTracker(localDcName, mockPartition, crossColoEnabled, successTarget, parallelism);
break;
case ADAPTIVE_OP_TRACKER:
tracker =
new AdaptiveOperationTracker(localDcName, mockPartition, crossColoEnabled, successTarget, parallelism, time,
localColoTracker, crossColoEnabled ? crossColoTracker : null, pastDueCounter, QUANTILE);
break;
default:
throw new IllegalArgumentException("Unrecognized operation tracker type - " + operationTrackerType);
}
return tracker;
}
/**
* Send requests to all replicas provided by the {@link OperationTracker#getReplicaIterator()}
* @param operationTracker the {@link OperationTracker} that provides replicas.
* @param numRequestsExpected the number of requests expected to be sent out.
* @param skipAlternate {@code true} if alternate {@link ReplicaId} instances from the iterator have to be skipped
* (i.e. no requests sent).
*/
private void sendRequests(OperationTracker operationTracker, int numRequestsExpected, boolean skipAlternate) {
int counter = 0;
int sent = 0;
Iterator<ReplicaId> replicaIdIterator = operationTracker.getReplicaIterator();
while (replicaIdIterator.hasNext()) {
ReplicaId nextReplica = replicaIdIterator.next();
assertNotNull("There should be a replica to send a request to", nextReplica);
assertFalse("Replica that was used for a request returned by iterator again",
repetitionTracker.contains(nextReplica));
if (!skipAlternate || counter % 2 == 0) {
inflightReplicas.offer(nextReplica);
replicaIdIterator.remove();
repetitionTracker.add(nextReplica);
sent++;
}
counter++;
}
assertEquals("Did not send expected number of requests", numRequestsExpected, sent);
}
/**
* Test replica down scenario
* @param totalReplicaCount total replicas for the partition.
* @param downReplicaCount partitions to be marked down.
*/
private void testReplicaDown(int totalReplicaCount, int downReplicaCount) {
List<Boolean> downStatus = new ArrayList<>(totalReplicaCount);
for (int i = 0; i < downReplicaCount; i++) {
downStatus.add(true);
}
for (int i = downReplicaCount; i < totalReplicaCount; i++) {
downStatus.add(false);
}
Collections.shuffle(downStatus);
List<ReplicaId> mockReplicaIds = mockPartition.getReplicaIds();
for (int i = 0; i < totalReplicaCount; i++) {
((MockReplicaId) mockReplicaIds.get(i)).markReplicaDownStatus(downStatus.get(i));
}
localDcName = datanodes.get(0).getDatacenterName();
OperationTracker ot = getOperationTracker(true, 2, 3);
// The iterator should return all replicas, with the first half being the up replicas
// and the last half being the down replicas.
Iterator<ReplicaId> itr = ot.getReplicaIterator();
int count = 0;
while (itr.hasNext()) {
ReplicaId nextReplica = itr.next();
if (count < totalReplicaCount - downReplicaCount) {
assertFalse(nextReplica.isDown());
} else {
assertTrue(nextReplica.isDown());
}
count++;
}
assertEquals("Total replica count did not match expected", totalReplicaCount, count);
}
}