/*
* Copyright 2008-2013 LinkedIn, Inc
*
* 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. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package voldemort.server.storage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.mutable.MutableBoolean;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import voldemort.TestUtils;
import voldemort.VoldemortTestConstants;
import voldemort.cluster.Cluster;
import voldemort.routing.StoreRoutingPlan;
import voldemort.server.storage.prunejob.VersionedPutPruneJob;
import voldemort.store.StoreDefinition;
import voldemort.utils.Time;
import voldemort.versioning.ChainedResolver;
import voldemort.versioning.Occurred;
import voldemort.versioning.TimeBasedInconsistencyResolver;
import voldemort.versioning.VectorClock;
import voldemort.versioning.VectorClockInconsistencyResolver;
import voldemort.versioning.VectorClockUtils;
import voldemort.versioning.Versioned;
import voldemort.xml.StoreDefinitionsMapper;
import com.google.common.collect.Lists;
public class VersionedPutPruningTest {
Cluster cluster;
StoreDefinition storeDef;
StoreRoutingPlan routingPlan;
byte[] key;
List<Integer> keyReplicas;
@Before
public void setup() throws Exception {
cluster = VoldemortTestConstants.getNineNodeCluster();
StringReader reader = new StringReader(VoldemortTestConstants.getSingleStore322Xml());
storeDef = new StoreDefinitionsMapper().readStoreList(reader).get(0);
routingPlan = new StoreRoutingPlan(cluster, storeDef);
key = "key1".getBytes();
keyReplicas = Lists.newArrayList(0, 2, 1);
}
@Test
public void testPruningLogic() {
List<Versioned<byte[]>> vals = new ArrayList<Versioned<byte[]>>();
VectorClock clock1 = TestUtils.getClock(0, 2, 1, 3);
VectorClock clock2 = TestUtils.getClock(9, 4);
VectorClock clock3 = TestUtils.getClock(0, 1);
VectorClock clock4 = TestUtils.getClock(8, 0);
vals.add(new Versioned<byte[]>(key, clock1));
vals.add(new Versioned<byte[]>(key, clock2));
vals.add(new Versioned<byte[]>(key, clock3));
vals.add(new Versioned<byte[]>(key, clock4));
MutableBoolean didPrune = new MutableBoolean(false);
List<Versioned<byte[]>> prunedVals = VersionedPutPruneJob.pruneNonReplicaEntries(vals,
keyReplicas,
didPrune);
assertEquals("Must have pruned some versions", true, didPrune.booleanValue());
assertEquals("Not pruned properly", TestUtils.getClock(0, 1, 2), prunedVals.get(0)
.getVersion());
assertEquals("Not pruned properly", TestUtils.getClock(), prunedVals.get(1).getVersion());
assertEquals("Not pruned properly", TestUtils.getClock(0, 1), prunedVals.get(2)
.getVersion());
assertEquals("Not pruned properly", TestUtils.getClock(0), prunedVals.get(3).getVersion());
List<Versioned<byte[]>> resolvedVals = VectorClockUtils.resolveVersions(prunedVals);
assertEquals("Must be exactly one winning version", 1, resolvedVals.size());
assertEquals("Incorrect winning version",
TestUtils.getClock(0, 1, 2),
resolvedVals.get(0).getVersion());
assertEquals("Incorrect winning version",
clock1.getTimestamp(),
((VectorClock) resolvedVals.get(0).getVersion()).getTimestamp());
}
private List<Versioned<byte[]>> pruneAndResolve(List<Versioned<byte[]>> vals,
MutableBoolean didPrune) {
return VectorClockUtils.resolveVersions(VersionedPutPruneJob.pruneNonReplicaEntries(vals,
keyReplicas,
didPrune));
}
@Test
public void testOnlineBehavior() {
long now = System.currentTimeMillis();
// let's assume previous replicas are [4, 5, 0]
VectorClock fetchedClock = TestUtils.getVersionedPutClock(now, 4, 4, 5, 0);
VectorClock onlineClock = TestUtils.getVersionedPutClock(now, 0, 0, 2, 1);
assertEquals("fetched and online versions should conflict",
Occurred.CONCURRENTLY,
VectorClockUtils.compare(fetchedClock, onlineClock));
// case where key has received writes before the prune job
List<Versioned<byte[]>> vals = new ArrayList<Versioned<byte[]>>();
vals.add(new Versioned<byte[]>(key, fetchedClock));
vals.add(new Versioned<byte[]>(key, onlineClock));
MutableBoolean didPrune = new MutableBoolean();
vals = pruneAndResolve(vals, didPrune);
assertEquals("Must have pruned something", true, didPrune.booleanValue());
assertEquals("Must have one winning version", 1, vals.size());
assertEquals("Must resolve to onlineClock", onlineClock, vals.get(0).getVersion());
// case where key has not received any writes before the prune job
vals = new ArrayList<Versioned<byte[]>>();
vals.add(new Versioned<byte[]>(key, fetchedClock));
didPrune = new MutableBoolean();
vals = pruneAndResolve(vals, didPrune);
assertEquals("Must have pruned something", true, didPrune.booleanValue());
assertEquals("Must have one winning version", 1, vals.size());
assertEquals("Must resolve to [0:ts] clock",
TestUtils.getVersionedPutClock(now, -1, 0),
vals.get(0).getVersion());
VectorClock nextOnlineClock = TestUtils.getVersionedPutClock(now + Time.MS_PER_SECOND,
0,
0,
2,
1);
assertFalse("Next online write would not result in conflict",
Occurred.CONCURRENTLY == VectorClockUtils.compare((VectorClock) vals.get(0)
.getVersion(),
nextOnlineClock));
}
@Test
public void testOnlineBehaviorWithConflicts() {
long now = System.currentTimeMillis();
// let's assume previous replicas are [4, 5, 0]. Note the intersection
// between old and new replicas
VectorClock fetchedClock1 = TestUtils.getVersionedPutClock(now, 4, 4, 5, 0);
VectorClock fetchedClock2 = TestUtils.getVersionedPutClock(now, 5, 4, 5, 0);
// to tighen the screws, assume the fetch delay was 0ms and two
// conflicting writes followed on the same ms
VectorClock onlineClock1 = TestUtils.getVersionedPutClock(now, 0, 0, 2, 1);
VectorClock onlineClock2 = TestUtils.getVersionedPutClock(now, 2, 0, 2, 1);
assertEquals("fetched versions themselves should conflict",
Occurred.CONCURRENTLY,
VectorClockUtils.compare(fetchedClock1, fetchedClock2));
assertEquals("online versions themselves should conflict",
Occurred.CONCURRENTLY,
VectorClockUtils.compare(onlineClock1, onlineClock2));
assertEquals("fetched and online versions should conflict",
Occurred.CONCURRENTLY,
VectorClockUtils.compare(fetchedClock1, onlineClock1));
assertEquals("fetched and online versions should conflict",
Occurred.CONCURRENTLY,
VectorClockUtils.compare(fetchedClock1, onlineClock1));
assertEquals("fetched and online versions should conflict",
Occurred.CONCURRENTLY,
VectorClockUtils.compare(fetchedClock1, onlineClock2));
assertEquals("fetched and online versions should conflict",
Occurred.CONCURRENTLY,
VectorClockUtils.compare(fetchedClock2, onlineClock1));
assertEquals("fetched and online versions should conflict",
Occurred.CONCURRENTLY,
VectorClockUtils.compare(fetchedClock2, onlineClock2));
// case where key has received writes before the prune job
List<Versioned<byte[]>> vals = new ArrayList<Versioned<byte[]>>();
vals.add(new Versioned<byte[]>(key, fetchedClock1));
vals.add(new Versioned<byte[]>(key, onlineClock1));
vals.add(new Versioned<byte[]>(key, fetchedClock2));
vals.add(new Versioned<byte[]>(key, onlineClock2));
MutableBoolean didPrune = new MutableBoolean();
vals = pruneAndResolve(vals, didPrune);
assertEquals("Must have pruned something", true, didPrune.booleanValue());
assertEquals("Must have two winning versions", 2, vals.size());
assertEquals("Must have onlineClock1", onlineClock1, vals.get(0).getVersion());
assertEquals("Must have onlineClock2", onlineClock2, vals.get(1).getVersion());
// case where key has not received any writes before the prune job
vals = new ArrayList<Versioned<byte[]>>();
vals.add(new Versioned<byte[]>(key, fetchedClock1));
vals.add(new Versioned<byte[]>(key, fetchedClock2));
didPrune = new MutableBoolean();
vals = pruneAndResolve(vals, didPrune);
assertEquals("Must have pruned something", true, didPrune.booleanValue());
// Note that since 0 is not a master in both fetched clocks, there will
// be one version. If 0 were to be a master, there will be one version,
// since master clock will trump non-master clock
assertEquals("Must have one winning version", 1, vals.size());
assertEquals("Must resolve to [0:ts] clock",
TestUtils.getVersionedPutClock(now, -1, 0),
vals.get(0).getVersion());
VectorClock nextOnlineClock1 = TestUtils.getVersionedPutClock(now, 0, 0, 2, 1);
VectorClock nextOnlineClock2 = TestUtils.getVersionedPutClock(now, 2, 0, 2, 1);
assertFalse("Next online write would not result in conflict",
Occurred.CONCURRENTLY == VectorClockUtils.compare((VectorClock) vals.get(0)
.getVersion(),
nextOnlineClock1));
assertFalse("Next online write would not result in conflict",
Occurred.CONCURRENTLY == VectorClockUtils.compare((VectorClock) vals.get(0)
.getVersion(),
nextOnlineClock2));
}
@Test
public void testOutOfSyncPruning() {
long now = System.currentTimeMillis();
byte[] onlineVal = "online".getBytes();
ChainedResolver<Versioned<byte[]>> resolver = new ChainedResolver<Versioned<byte[]>>(new VectorClockInconsistencyResolver<byte[]>(),
new TimeBasedInconsistencyResolver<byte[]>());
// let's assume previous replicas are [4, 5, 0]
VectorClock fetchedClock = TestUtils.getVersionedPutClock(now, 4, 4, 5, 0);
VectorClock onlineClock1 = TestUtils.getVersionedPutClock(now + Time.MS_PER_SECOND,
0,
0,
2,
1);
// Both the servers have the two versions.
List<Versioned<byte[]>> vals1 = new ArrayList<Versioned<byte[]>>();
vals1.add(new Versioned<byte[]>(key, fetchedClock));
vals1.add(new Versioned<byte[]>(onlineVal, onlineClock1));
List<Versioned<byte[]>> vals2 = new ArrayList<Versioned<byte[]>>();
vals2.add(new Versioned<byte[]>(key, fetchedClock));
vals2.add(new Versioned<byte[]>(onlineVal, onlineClock1));
// job fixes server 1
vals1 = pruneAndResolve(vals1, new MutableBoolean());
assertEquals("Must have one winning version", 1, vals1.size());
assertEquals("Must resolve to onlineClock", onlineClock1, vals1.get(0).getVersion());
// reads only from pruned server
List<Versioned<byte[]>> resolvedVersions = resolver.resolveConflicts(vals1);
assertEquals("Must read out latest timestamp version",
onlineClock1.getTimestamp(),
((VectorClock) resolvedVersions.get(0).getVersion()).getTimestamp());
assertEquals("Online value to be read out",
new String(onlineVal),
new String(resolvedVersions.get(0).getValue()));
// reads only from non pruned server
resolvedVersions = resolver.resolveConflicts(vals2);
// Note : check on timestamp is meaningless when actual merging is
// involved , since merging clocks will result in
// System.currentTimeMillis() being used for the merged vector clock
assertEquals("Online value to be read out",
new String(onlineVal),
new String(resolvedVersions.get(0).getValue()));
// reads combining both
List<Versioned<byte[]>> vals = new ArrayList<Versioned<byte[]>>();
vals.addAll(vals1);
vals.addAll(vals2);
resolvedVersions = resolver.resolveConflicts(vals);
assertEquals("Online value to be read out",
new String(onlineVal),
new String(resolvedVersions.get(0).getValue()));
}
@After
public void teardown() {
}
}