GOSSIP-64 Implement Max-Change-Sets

This commit is contained in:
Maxim Rusak
2017-06-26 03:34:23 +03:00
committed by edward
parent 49cdac62a2
commit 1769449515
12 changed files with 660 additions and 410 deletions

View File

@ -0,0 +1,12 @@
package org.apache.gossip.crdt;
import java.util.Set;
// Interface extends CrdtSet interface with add and remove operation that are guaranteed to be immutable.
// If your implementation provide immutable add/remove operations you can extend AbstractCRDTStringSetTest to check it in the most ways.
public interface CrdtAddRemoveSet<T, SetType extends Set<T>, R extends CrdtAddRemoveSet<T, SetType, R>> extends CrdtSet<T, SetType, R> {
R add(T element);
R remove(T element);
}

View File

@ -17,16 +17,16 @@
*/
package org.apache.gossip.crdt;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
abstract class OrSetMixin<E> {
@JsonCreator
OrSetMixin(@JsonProperty("elements") Map<E, Set<UUID>> w, @JsonProperty("tombstones") Map<E, Set<UUID>> h) { }
@ -37,8 +37,8 @@ abstract class OrSetMixin<E> {
abstract class LWWSetMixin<ElementType> {
@JsonCreator
LWWSetMixin(@JsonProperty("data") Map<ElementType, LWWSet.Timestamps> struct) { }
@JsonProperty("data") abstract Map<ElementType, LWWSet.Timestamps> getStruct();
LWWSetMixin(@JsonProperty("data") Map<ElementType, LwwSet.Timestamps> struct) { }
@JsonProperty("data") abstract Map<ElementType, LwwSet.Timestamps> getStruct();
}
abstract class LWWSetTimestampsMixin {
@ -48,6 +48,12 @@ abstract class LWWSetTimestampsMixin {
@JsonProperty("remove") abstract long getLatestRemove();
}
abstract class MaxChangeSetMixin<E> {
@JsonCreator
MaxChangeSetMixin(@JsonProperty("data") Map<E, Integer> struct) { }
@JsonProperty("data") abstract Map<E, Integer> getStruct();
}
abstract class GrowOnlySetMixin<E>{
@JsonCreator
GrowOnlySetMixin(@JsonProperty("elements") Set<E> elements){ }
@ -84,8 +90,9 @@ public class CrdtModule extends SimpleModule {
context.setMixInAnnotations(GrowOnlySet.class, GrowOnlySetMixin.class);
context.setMixInAnnotations(GrowOnlyCounter.class, GrowOnlyCounterMixin.class);
context.setMixInAnnotations(PNCounter.class, PNCounterMixin.class);
context.setMixInAnnotations(LWWSet.class, LWWSetMixin.class);
context.setMixInAnnotations(LWWSet.Timestamps.class, LWWSetTimestampsMixin.class);
context.setMixInAnnotations(LwwSet.class, LWWSetMixin.class);
context.setMixInAnnotations(LwwSet.Timestamps.class, LWWSetTimestampsMixin.class);
context.setMixInAnnotations(MaxChangeSet.class, MaxChangeSetMixin.class);
}
}

View File

@ -20,12 +20,31 @@ package org.apache.gossip.crdt;
import org.apache.gossip.manager.Clock;
import org.apache.gossip.manager.SystemClock;
import java.util.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class LWWSet<ElementType> implements CrdtSet<ElementType, Set<ElementType>, LWWSet<ElementType>> {
/*
Last write wins CrdtSet
Each operation has timestamp: when you add or remove SystemClock is used to get current time in nanoseconds.
When all add/remove operations are within the only node LWWSet is guaranteed to work like a Set.
If you have multiple nodes with ideally synchronized clocks:
You will observe operations on all machines later than on the initiator, but the last operations on cluster will win.
If you have some significant clock drift you will suffer from data loss.
Read more: https://github.com/aphyr/meangirls#lww-element-set
You can view examples of usage in tests:
LwwSetTest - unit tests
DataTest - integration test with 2 nodes, LWWSet was serialized/deserialized, sent between nodes, merged
*/
public class LwwSet<ElementType> implements CrdtAddRemoveSet<ElementType, Set<ElementType>, LwwSet<ElementType>> {
static private Clock clock = new SystemClock();
private final Map<ElementType, Timestamps> struct;
@ -44,11 +63,11 @@ public class LWWSet<ElementType> implements CrdtSet<ElementType, Set<ElementType
latestRemove = remove;
}
long getLatestAdd() {
long getLatestAdd(){
return latestAdd;
}
long getLatestRemove() {
long getLatestRemove(){
return latestRemove;
}
@ -74,23 +93,23 @@ public class LWWSet<ElementType> implements CrdtSet<ElementType, Set<ElementType
}
public LWWSet(){
public LwwSet(){
struct = new HashMap<>();
}
@SafeVarargs
public LWWSet(ElementType... elements){
public LwwSet(ElementType... elements){
this(new HashSet<>(Arrays.asList(elements)));
}
public LWWSet(Set<ElementType> set){
public LwwSet(Set<ElementType> set){
struct = new HashMap<>();
for (ElementType e : set){
struct.put(e, new Timestamps().updateAdd());
}
}
public LWWSet(LWWSet<ElementType> first, LWWSet<ElementType> second){
public LwwSet(LwwSet<ElementType> first, LwwSet<ElementType> second){
Function<ElementType, Timestamps> timestampsFor = p -> {
Timestamps firstTs = first.struct.get(p);
Timestamps secondTs = second.struct.get(p);
@ -103,33 +122,33 @@ public class LWWSet<ElementType> implements CrdtSet<ElementType, Set<ElementType
.distinct().collect(Collectors.toMap(p -> p, timestampsFor));
}
public LWWSet<ElementType> add(ElementType e){
return this.merge(new LWWSet<>(e));
public LwwSet<ElementType> add(ElementType e){
return this.merge(new LwwSet<>(e));
}
// for serialization
LWWSet(Map<ElementType, Timestamps> struct){
LwwSet(Map<ElementType, Timestamps> struct){
this.struct = struct;
}
Map<ElementType, Timestamps> getStruct() {
Map<ElementType, Timestamps> getStruct(){
return struct;
}
public LWWSet<ElementType> remove(ElementType e){
public LwwSet<ElementType> remove(ElementType e){
Timestamps eTimestamps = struct.get(e);
if (eTimestamps == null || !eTimestamps.isPresent()){
return this;
}
Map<ElementType, Timestamps> changeMap = new HashMap<>();
changeMap.put(e, eTimestamps.updateRemove());
return this.merge(new LWWSet<>(changeMap));
return this.merge(new LwwSet<>(changeMap));
}
@Override
public LWWSet<ElementType> merge(LWWSet<ElementType> other){
return new LWWSet<>(this, other);
public LwwSet<ElementType> merge(LwwSet<ElementType> other){
return new LwwSet<>(this, other);
}
@Override
@ -141,12 +160,12 @@ public class LWWSet<ElementType> implements CrdtSet<ElementType, Set<ElementType
}
@Override
public LWWSet<ElementType> optimize(){
public LwwSet<ElementType> optimize(){
return this;
}
@Override
public boolean equals(Object obj){
return this == obj || (obj != null && getClass() == obj.getClass() && value().equals(((LWWSet) obj).value()));
return this == obj || (obj != null && getClass() == obj.getClass() && value().equals(((LwwSet) obj).value()));
}
}

View File

@ -0,0 +1,117 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.gossip.crdt;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/*
Max Change Set CrdtSet. Value which has changed the most wins.
You cannot delete an element which is not present, and cannot add an element which is already present.
MC-sets are compact and do the right thing when changes to elements are infrequent compared to the gossiping period.
Read more: https://github.com/aphyr/meangirls#max-change-sets
You can view examples of usage in tests:
MaxChangeSetTest - unit tests
DataTest - integration test with 2 nodes, MaxChangeSet was serialized/deserialized, sent between nodes, merged
*/
public class MaxChangeSet<ElementType> implements CrdtAddRemoveSet<ElementType, Set<ElementType>, MaxChangeSet<ElementType>> {
private final Map<ElementType, Integer> struct;
public MaxChangeSet(){
struct = new HashMap<>();
}
@SafeVarargs
public MaxChangeSet(ElementType... elements){
this(new HashSet<>(Arrays.asList(elements)));
}
public MaxChangeSet(Set<ElementType> set){
struct = new HashMap<>();
for (ElementType e : set){
struct.put(e, 1);
}
}
public MaxChangeSet(MaxChangeSet<ElementType> first, MaxChangeSet<ElementType> second){
Function<ElementType, Integer> valueFor = element ->
Math.max(first.struct.getOrDefault(element, 0), second.struct.getOrDefault(element, 0));
struct = Stream.concat(first.struct.keySet().stream(), second.struct.keySet().stream())
.distinct().collect(Collectors.toMap(p -> p, valueFor));
}
// for serialization
MaxChangeSet(Map<ElementType, Integer> struct){
this.struct = struct;
}
Map<ElementType, Integer> getStruct(){
return struct;
}
private MaxChangeSet<ElementType> increment(ElementType e){
Map<ElementType, Integer> changeMap = new HashMap<>();
changeMap.put(e, struct.getOrDefault(e, 0) + 1);
return this.merge(new MaxChangeSet<>(changeMap));
}
public MaxChangeSet<ElementType> add(ElementType e){
if (struct.getOrDefault(e, 0) % 2 == 1){
return this;
}
return increment(e);
}
public MaxChangeSet<ElementType> remove(ElementType e){
if (struct.getOrDefault(e, 0) % 2 == 0){
return this;
}
return increment(e);
}
@Override
public MaxChangeSet<ElementType> merge(MaxChangeSet<ElementType> other){
return new MaxChangeSet<>(this, other);
}
@Override
public Set<ElementType> value(){
return struct.entrySet().stream()
.filter(entry -> (entry.getValue() % 2 == 1))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
@Override
public MaxChangeSet<ElementType> optimize(){
return this;
}
@Override
public boolean equals(Object obj){
return this == obj || (obj != null && getClass() == obj.getClass() && value().equals(((MaxChangeSet) obj).value()));
}
}

View File

@ -26,7 +26,7 @@ import org.apache.gossip.crdt.OrSet.Builder.Operation;
/*
* A immutable set
*/
public class OrSet<E> implements Crdt<Set<E>, OrSet<E>> {
public class OrSet<E> implements CrdtAddRemoveSet<E, Set<E>, OrSet<E>> {
private final Map<E, Set<UUID>> elements = new HashMap<>();
private final Map<E, Set<UUID>> tombstones = new HashMap<>();
@ -44,6 +44,10 @@ public class OrSet<E> implements Crdt<Set<E>, OrSet<E>> {
@SafeVarargs
public OrSet(E ... elements){
this(new HashSet<>(Arrays.asList(elements)));
}
public OrSet(Set<E> elements) {
for (E e: elements){
internalAdd(e);
}
@ -109,7 +113,15 @@ public class OrSet<E> implements Crdt<Set<E>, OrSet<E>> {
val = computeValue();
}
public OrSet<E> add(E e) {
return this.merge(new OrSet<>(e));
}
public OrSet<E> remove(E e) {
return new OrSet<>(this, new Builder<E>().remove(e));
}
public OrSet.Builder<E> builder(){
return new OrSet.Builder<>();
}
@ -233,15 +245,6 @@ public class OrSet<E> implements Crdt<Set<E>, OrSet<E>> {
return value().toArray(a);
}
public boolean add(E e) {
throw new IllegalArgumentException("Can not add");
}
public boolean remove(Object o) {
throw new IllegalArgumentException();
}
public boolean containsAll(Collection<?> c) {
return this.value().containsAll(c);
}

View File

@ -0,0 +1,133 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* Unle<F4>ss 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 org.apache.gossip.crdt;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.Ignore;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/*
Abstract test suit to test CrdtSets with Add and Remove operations.
It compares them with simple sets, validates add, remove, equals, value, etc. operations
To use it you should:
1. subclass this and implement constructors
2. implement CrdtAddRemoveSet in your CrdtSet
3. make your CrdtSet immutable
*/
@Ignore
public abstract class AbstractCRDTStringSetTest<SetType extends CrdtAddRemoveSet<String, Set<String>, SetType>> {
abstract SetType construct(Set<String> set);
abstract SetType construct();
private Set<String> sampleSet;
@Before
public void setup(){
sampleSet = new HashSet<>();
sampleSet.add("4");
sampleSet.add("5");
sampleSet.add("12");
}
@Test
public void abstractSetConstructorTest(){
Assert.assertEquals(construct(sampleSet).value(), sampleSet);
}
@Test
public void abstractStressWithSetTest(){
Set<String> hashSet = new HashSet<>();
SetType set = construct();
for (int it = 0; it < 40; it++){
SetType newSet;
if (it % 5 == 1){
//deleting existing
String forDelete = hashSet.stream().skip((long) (hashSet.size() * Math.random())).findFirst().get();
newSet = set.remove(forDelete);
Assert.assertEquals(set.value(), hashSet); // check old version is immutable
hashSet.remove(forDelete);
} else {
//adding
String forAdd = String.valueOf((int) (10000 * Math.random()));
newSet = set.add(forAdd);
Assert.assertEquals(set.value(), hashSet); // check old version is immutable
hashSet.add(forAdd);
}
set = newSet;
Assert.assertEquals(set.value(), hashSet);
}
}
@Test
public void abstractEqualsTest(){
SetType set = construct(sampleSet);
Assert.assertFalse(set.equals(sampleSet));
SetType newSet = set.add("25");
sampleSet.add("25");
Assert.assertFalse(newSet.equals(set));
Assert.assertEquals(construct(sampleSet), newSet);
}
@Test
public void abstractRemoveMissingTest(){
SetType set = construct(sampleSet);
set = set.add("25");
set = set.remove("25");
Assert.assertEquals(set.value(), sampleSet);
set = set.remove("25");
set = set.add("25");
sampleSet.add("25");
Assert.assertEquals(set.value(), sampleSet);
}
@Test
public void abstractStressMergeTest(){
// in one-process context, add, remove and merge operations of lww are equal to operations of Set
// we've already checked it. Now just check merge
Set<String> hashSet1 = new HashSet<>(), hashSet2 = new HashSet<>();
SetType set1 = construct(), set2 = construct();
for (int it = 0; it < 100; it++){
String forAdd = String.valueOf((int) (10000 * Math.random()));
if (it % 2 == 0){
hashSet1.add(forAdd);
set1 = set1.add(forAdd);
} else {
hashSet2.add(forAdd);
set2 = set2.add(forAdd);
}
}
Assert.assertEquals(set1.value(), hashSet1);
Assert.assertEquals(set2.value(), hashSet2);
Set<String> mergedSet = Stream.concat(hashSet1.stream(), hashSet2.stream()).collect(Collectors.toSet());
Assert.assertEquals(set1.merge(set2).value(), mergedSet);
}
@Test
public void abstractOptimizeTest(){
Assert.assertEquals(construct(sampleSet).value(), sampleSet);
Assert.assertEquals(construct(sampleSet).optimize().value(), sampleSet);
}
}

View File

@ -1,155 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.gossip.crdt;
import org.apache.gossip.manager.Clock;
import org.apache.gossip.manager.SystemClock;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class LWWSetTest {
static private Clock clock = new SystemClock();
private Set<Integer> sampleSet;
@Before
public void setup(){
sampleSet = new HashSet<>();
sampleSet.add(4);
sampleSet.add(5);
sampleSet.add(12);
}
@Test
public void setConstructorTest(){
Assert.assertEquals(new LWWSet<>(sampleSet).value(), sampleSet);
}
@Test
public void stressWithSetTest(){
Set<Integer> set = new HashSet<>();
LWWSet<Integer> lww = new LWWSet<>();
for (int it = 0; it < 100; it++){
LWWSet<Integer> newLww;
if (it % 5 == 1){
//deleting existing
Integer forDelete = set.stream().skip((long) (set.size() * Math.random())).findFirst().get();
newLww = lww.remove(forDelete);
Assert.assertEquals(lww.value(), set); // check old version is immutable
set.remove(forDelete);
} else {
//adding
Integer forAdd = (int) (10000 * Math.random());
newLww = lww.add(forAdd);
Assert.assertEquals(lww.value(), set); // check old version is immutable
set.add(forAdd);
}
lww = newLww;
Assert.assertEquals(lww.value(), set);
}
}
@Test
public void equalsTest(){
LWWSet<Integer> lww = new LWWSet<>(sampleSet);
Assert.assertFalse(lww.equals(sampleSet));
LWWSet<Integer> newLww = lww.add(25);
sampleSet.add(25);
Assert.assertFalse(newLww.equals(lww));
Assert.assertEquals(new LWWSet<>(sampleSet), newLww);
}
@Test
public void valueTest() {
Map<Character, LWWSet.Timestamps> map = new HashMap<>();
map.put('a', new LWWSet.Timestamps(1, 0));
map.put('b', new LWWSet.Timestamps(1, 2));
map.put('c', new LWWSet.Timestamps(3, 3));
Set<Character> toTest = new HashSet<>();
toTest.add('a'); // for 'a' addTime > removeTime
toTest.add('c'); // for 'c' times are equal, we prefer add to remove
Assert.assertEquals(new LWWSet<>(map).value(), toTest);
Assert.assertEquals(new LWWSet<>(map), new LWWSet<>('a', 'c'));
}
@Test
public void removeMissingTest(){
LWWSet<Integer> lww = new LWWSet<>(sampleSet);
lww = lww.add(25);
lww = lww.remove(25);
Assert.assertEquals(lww.value(), sampleSet);
lww = lww.remove(25);
lww = lww.add(25);
sampleSet.add(25);
Assert.assertEquals(lww.value(), sampleSet);
}
@Test
public void stressMergeTest(){
// in one-process context, add, remove and merge operations of lww are equal to operations of Set
// we've already checked it. Now just check merge
Set<Integer> set1 = new HashSet<>(), set2 = new HashSet<>();
LWWSet<Integer> lww1 = new LWWSet<>(), lww2 = new LWWSet<>();
for (int it = 0; it < 100; it++){
Integer forAdd = (int) (10000 * Math.random());
if (it % 2 == 0){
set1.add(forAdd);
lww1 = lww1.add(forAdd);
} else {
set2.add(forAdd);
lww2 = lww2.add(forAdd);
}
}
Assert.assertEquals(lww1.value(), set1);
Assert.assertEquals(lww2.value(), set2);
Set<Integer> mergedSet = Stream.concat(set1.stream(), set2.stream()).collect(Collectors.toSet());
Assert.assertEquals(lww1.merge(lww2).value(), mergedSet);
}
@Test
public void fakeTimeMergeTest(){
// try to create LWWSet with time from future (simulate other process with its own clock) and validate result
// check remove from the future
Map<Integer, LWWSet.Timestamps> map = new HashMap<>();
map.put(25, new LWWSet.Timestamps(clock.nanoTime(), clock.nanoTime() + 100000));
LWWSet<Integer> lww = new LWWSet<>(map);
Assert.assertEquals(lww, new LWWSet<Integer>());
//create new LWWSet with element 25, and merge with other LWW which has remove in future
Assert.assertEquals(new LWWSet<>(25).merge(lww), new LWWSet<Integer>());
// add in future
map.put(25, new LWWSet.Timestamps(clock.nanoTime() + 100000, 0));
lww = new LWWSet<>(map);
lww = lww.remove(25);
Assert.assertEquals(lww, new LWWSet<>(25)); // 25 is still here
}
@Test
public void optimizeTest(){
Assert.assertEquals(new LWWSet<>(sampleSet).value(), sampleSet);
Assert.assertEquals(new LWWSet<>(sampleSet).optimize().value(), sampleSet);
}
}

View File

@ -0,0 +1,71 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.gossip.crdt;
import org.apache.gossip.manager.Clock;
import org.apache.gossip.manager.SystemClock;
import org.junit.Assert;
import org.junit.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class LwwSetTest extends AbstractCRDTStringSetTest<LwwSet<String>> {
static private Clock clock = new SystemClock();
LwwSet<String> construct(Set<String> set){
return new LwwSet<>(set);
}
LwwSet<String> construct(){
return new LwwSet<>();
}
@Test
public void valueTest(){
Map<Character, LwwSet.Timestamps> map = new HashMap<>();
map.put('a', new LwwSet.Timestamps(1, 0));
map.put('b', new LwwSet.Timestamps(1, 2));
map.put('c', new LwwSet.Timestamps(3, 3));
Set<Character> toTest = new HashSet<>();
toTest.add('a'); // for 'a' addTime > removeTime
toTest.add('c'); // for 'c' times are equal, we prefer add to remove
Assert.assertEquals(new LwwSet<>(map).value(), toTest);
Assert.assertEquals(new LwwSet<>(map), new LwwSet<>('a', 'c'));
}
@Test
public void fakeTimeMergeTest(){
// try to create LWWSet with time from future (simulate other process with its own clock) and validate result
// check remove from the future
Map<Integer, LwwSet.Timestamps> map = new HashMap<>();
map.put(25, new LwwSet.Timestamps(clock.nanoTime(), Long.MAX_VALUE));
LwwSet<Integer> lww = new LwwSet<>(map);
Assert.assertEquals(lww, new LwwSet<Integer>());
//create new LWWSet with element 25, and merge with other LWW which has remove in future
Assert.assertEquals(new LwwSet<>(25).merge(lww), new LwwSet<Integer>());
// add in future
map.put(25, new LwwSet.Timestamps(Long.MAX_VALUE, 0));
lww = new LwwSet<>(map);
lww = lww.remove(25);
Assert.assertEquals(lww, new LwwSet<>(25)); // 25 is still here
}
}

View File

@ -0,0 +1,67 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.gossip.crdt;
import org.junit.Assert;
import org.junit.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class MaxChangeSetTest extends AbstractCRDTStringSetTest<MaxChangeSet<String>> {
MaxChangeSet<String> construct(Set<String> set){
return new MaxChangeSet<>(set);
}
MaxChangeSet<String> construct(){
return new MaxChangeSet<>();
}
@Test
public void valueTest(){
Map<Character, Integer> struct = new HashMap<>();
struct.put('a', 0);
struct.put('b', 1);
struct.put('c', 2);
struct.put('d', 3);
Set<Character> result = new HashSet<>();
result.add('b');
result.add('d');
Assert.assertEquals(new MaxChangeSet<>(struct).value(), result);
}
@Test
public void mergeTest(){
MaxChangeSet<Integer> set1 = new MaxChangeSet<Integer>().add(1); // Set with one operation on 1
MaxChangeSet<Integer> set2 = new MaxChangeSet<Integer>().add(1).remove(1); // two operations
Assert.assertEquals(set1.merge(set2), new MaxChangeSet<Integer>()); // empty set wins
set1 = set1.add(1).add(1).add(1);
// empty set still wins, repetitive operations do nothing, don't increase number of operations
Assert.assertEquals(set1.merge(set2), new MaxChangeSet<Integer>());
set1 = set1.remove(1).add(1); // 3 operations
Assert.assertEquals(set1.merge(set2), new MaxChangeSet<>(1)); // full set wins now
set2 = set2.remove(1).remove(1).remove(1);
// full set still wins, repetitive removes don't increase number of operations too
Assert.assertEquals(set1.merge(set2), new MaxChangeSet<>(1));
}
}

View File

@ -17,36 +17,44 @@
*/
package org.apache.gossip.crdt;
import java.util.Arrays;
import java.util.SortedSet;
import java.util.TreeSet;
import org.junit.Assert;
import org.junit.Test;
public class OrSetTest {
import java.util.Arrays;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
public class OrSetTest extends AbstractCRDTStringSetTest<OrSet<String>> {
OrSet<String> construct(){
return new OrSet<>();
}
OrSet<String> construct(Set<String> set){
return new OrSet<>(set);
}
@Test
public void atest() {
public void atest(){
OrSet<Integer> i = new OrSet<>(new OrSet.Builder<Integer>().add(4).add(5).add(6).remove(5));
Assert.assertArrayEquals(Arrays.asList(4, 6).toArray(), i.value().toArray());
}
@Test
public void mergeTest(){
OrSet<Integer> i = new OrSet<>(new OrSet.Builder<Integer>().add(4).add(5).add(6).remove(5));
Assert.assertArrayEquals(Arrays.asList(4, 6).toArray(), i.value().toArray());
OrSet<Integer> j = new OrSet<>(new OrSet.Builder<Integer>().add(9).add(4).add(5).remove(6));
OrSet<Integer> h = i.merge(j);
Assert.assertEquals(new OrSet<Integer>(4,6,9,5), h);
Assert.assertEquals(new OrSet<>(4, 6, 9, 5), h);
}
@Test
public void mergeTest2(){
OrSet<Integer> i = new OrSet<>(new OrSet.Builder<Integer>().add(5).add(4).remove(4).add(6));
Assert.assertEquals(new OrSet<Integer>(5,6), i);
Assert.assertEquals(new OrSet<>(5, 6), i);
SortedSet<Integer> tree = new TreeSet<>();
for (Integer in: i.value()){
for (Integer in : i.value()){
tree.add(in);
}
TreeSet<Integer> compare = new TreeSet<>();
@ -54,34 +62,34 @@ public class OrSetTest {
compare.add(6);
Assert.assertEquals(tree, compare);
}
@Test
public void mergeTest4() {
Assert.assertArrayEquals(new Integer[] {},
new OrSet<Integer>(new OrSet.Builder<Integer>().add(1).remove(1)).toArray());
public void mergeTest4(){
Assert.assertArrayEquals(new Integer[]{},
new OrSet<>(new OrSet.Builder<Integer>().add(1).remove(1)).toArray());
}
@Test
public void mergeTest3(){
OrSet<Integer> i = new OrSet<>(1);
OrSet<Integer> j = new OrSet<>(2);
OrSet<Integer> k = new OrSet<>(i.merge(j), new OrSet.Builder<Integer>().remove(1));
Assert.assertArrayEquals(new Integer[] { 2 }, i.merge(j).merge(k).toArray());
Assert.assertArrayEquals(new Integer[] { 2 }, j.merge(i).merge(k).toArray());
Assert.assertArrayEquals(new Integer[] { 2 }, k.merge(i).merge(j).toArray());
Assert.assertArrayEquals(new Integer[] { 2 }, k.merge(j).merge(i).toArray());
Assert.assertEquals(j , i.merge(j.merge(k)));
OrSet<Integer> k = new OrSet<>(i.merge(j), new OrSet.Builder<Integer>().remove(1));
Assert.assertArrayEquals(new Integer[]{2}, i.merge(j).merge(k).toArray());
Assert.assertArrayEquals(new Integer[]{2}, j.merge(i).merge(k).toArray());
Assert.assertArrayEquals(new Integer[]{2}, k.merge(i).merge(j).toArray());
Assert.assertArrayEquals(new Integer[]{2}, k.merge(j).merge(i).toArray());
Assert.assertEquals(j, i.merge(j.merge(k)));
}
@Test
public void mergeTest9(){
OrSet<Integer> i = new OrSet<>(19);
OrSet<Integer> j = i.merge(i);
Assert.assertEquals(i.value(), j.value());
}
@Test
public void mergeTestSame() {
public void mergeTestSame(){
OrSet<Integer> i = new OrSet<>(19);
OrSet<Integer> j = new OrSet<>(19);
OrSet<Integer> k = i.merge(j);