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);

View File

@ -17,69 +17,81 @@
*/
package org.apache.gossip;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import io.teknek.tunit.TUnit;
import org.apache.gossip.crdt.CrdtAddRemoveSet;
import org.apache.gossip.crdt.GrowOnlyCounter;
import org.apache.gossip.crdt.GrowOnlySet;
import org.apache.gossip.crdt.LWWSet;
import org.apache.gossip.crdt.LwwSet;
import org.apache.gossip.crdt.MaxChangeSet;
import org.apache.gossip.crdt.OrSet;
import org.apache.gossip.crdt.PNCounter;
import org.apache.gossip.manager.GossipManager;
import org.apache.gossip.manager.GossipManagerBuilder;
import org.apache.gossip.model.PerNodeDataMessage;
import org.apache.gossip.model.SharedDataMessage;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import io.teknek.tunit.TUnit;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
public class DataTest extends AbstractIntegrationBase {
public class DataTest {
private final String gCounterKey = "crdtgc";
private final String pnCounterKey = "crdtpn";
private String orSetKey = "cror";
private String lwwSetKey = "crlww";
private String gCounterKey = "crdtgc";
private String pnCounterKey = "crdtpn";
private static final List<GossipManager> clients = new ArrayList<>();
@BeforeClass
public static void initializeMembers() throws InterruptedException, UnknownHostException, URISyntaxException{
final int clusterMembers = 2;
@Test
public void dataTest() throws InterruptedException, UnknownHostException, URISyntaxException {
GossipSettings settings = new GossipSettings();
settings.setPersistRingState(false);
settings.setPersistDataState(false);
String cluster = UUID.randomUUID().toString();
int seedNodes = 1;
List<Member> startupMembers = new ArrayList<>();
for (int i = 1; i < seedNodes + 1; ++i) {
URI uri = new URI("udp://" + "127.0.0.1" + ":" + (50000 + i));
startupMembers.add(new RemoteMember(cluster, uri, i + ""));
for (int i = 0; i < clusterMembers; ++i){
int id = i + 1;
URI uri = new URI("udp://" + "127.0.0.1" + ":" + (50000 + id));
startupMembers.add(new RemoteMember(cluster, uri, id + ""));
}
final List<GossipManager> clients = new ArrayList<>();
final int clusterMembers = 2;
for (int i = 1; i < clusterMembers + 1; ++i) {
URI uri = new URI("udp://" + "127.0.0.1" + ":" + (50000 + i));
GossipManager gossipService = GossipManagerBuilder
.newBuilder()
.cluster(cluster).uri(uri)
.id(i + "")
.gossipMembers(startupMembers)
.gossipSettings(settings).build();
for (Member member : startupMembers){
GossipManager gossipService = GossipManagerBuilder.newBuilder().cluster(cluster).uri(member.getUri())
.id(member.getId()).gossipMembers(startupMembers).gossipSettings(settings).build();
clients.add(gossipService);
gossipService.init();
register(gossipService);
}
}
@AfterClass
public static void shutdownMembers(){
for (final GossipManager client : clients){
client.shutdown();
}
}
@Test
public void simpleDataTest(){
TUnit.assertThat(() -> {
int total = 0;
for (int i = 0; i < clusterMembers; ++i) {
total += clients.get(i).getLiveMembers().size();
for (GossipManager client : clients){
total += client.getLiveMembers().size();
}
return total;
}).afterWaitingAtMost(20, TimeUnit.SECONDS).isEqualTo(2);
}).afterWaitingAtMost(10, TimeUnit.SECONDS).isEqualTo(2);
clients.get(0).gossipPerNodeData(generatePerNodeMsg("a", "b"));
clients.get(0).gossipSharedData(generateSharedMsg("a", "c"));
@ -89,7 +101,7 @@ public class DataTest extends AbstractIntegrationBase {
return "";
else
return x.getPayload();
}).afterWaitingAtMost(20, TimeUnit.SECONDS).isEqualTo("b");
}).afterWaitingAtMost(10, TimeUnit.SECONDS).isEqualTo("b");
TUnit.assertThat(() -> {
SharedDataMessage x = clients.get(1).findSharedGossipData("a");
@ -97,175 +109,118 @@ public class DataTest extends AbstractIntegrationBase {
return "";
else
return x.getPayload();
}).afterWaitingAtMost(20, TimeUnit.SECONDS).isEqualTo("c");
givenDifferentDatumsInSet(clients);
assertThatListIsMerged(clients);
testOrSet(clients);
testLWWSet(clients);
testGrowOnlyCounter(clients);
testPNCounter(clients);
for (int i = 0; i < clusterMembers; ++i) {
clients.get(i).shutdown();
}
}).afterWaitingAtMost(10, TimeUnit.SECONDS).isEqualTo("c");
}
private void testOrSet(final List<GossipManager> clients) {
// populate
clients.get(0).merge(generateSharedMsg(orSetKey, new OrSet<>("1", "2")));
clients.get(1).merge(generateSharedMsg(orSetKey, new OrSet<>("3", "4")));
Set<String> setFromList(String... elements){
return new HashSet<>(Arrays.asList(elements));
}
// assert merge
assertMerged(clients.get(0), orSetKey, new OrSet<>("1", "2", "3", "4").value());
assertMerged(clients.get(1), orSetKey, new OrSet<>("1", "2", "3", "4").value());
void crdtSetTest(String key, Function<Set<String>, CrdtAddRemoveSet<String, Set<String>, ?>> construct){
//populate
clients.get(0).merge(generateSharedMsg(key, construct.apply(setFromList("1", "2"))));
clients.get(1).merge(generateSharedMsg(key, construct.apply(setFromList("3", "4"))));
// drop element
assertMergedCrdt(key, construct.apply(setFromList("1", "2", "3", "4")).value());
//drop element
@SuppressWarnings("unchecked")
OrSet<String> o = (OrSet<String>) clients.get(0).findCrdt(orSetKey);
OrSet<String> o2 = new OrSet<>(o, new OrSet.Builder<String>().remove("3"));
clients.get(0).merge(generateSharedMsg(orSetKey, o2));
CrdtAddRemoveSet<String, ?, ?> set = (CrdtAddRemoveSet<String, ?, ?>) clients.get(0).findCrdt(key);
clients.get(0).merge(generateSharedMsg(key, set.remove("3")));
// assert deletion
assertMerged(clients.get(0), orSetKey, new OrSet<>("1", "2", "4").value());
assertMerged(clients.get(1), orSetKey, new OrSet<>("1", "2", "4").value());
//assert deletion
assertMergedCrdt(key, construct.apply(setFromList("1", "2", "4")).value());
}
private void testLWWSet(final List<GossipManager> clients) {
// populate
clients.get(0).merge(generateSharedMsg(lwwSetKey, new LWWSet<>("1", "2")));
clients.get(1).merge(generateSharedMsg(lwwSetKey, new LWWSet<>("3", "4")));
// assert merge
assertMerged(clients.get(0), lwwSetKey, new LWWSet<>("1", "2", "3", "4").value());
assertMerged(clients.get(1), lwwSetKey, new LWWSet<>("1", "2", "3", "4").value());
// drop element
@SuppressWarnings("unchecked")
LWWSet<String> lww = (LWWSet<String>) clients.get(0).findCrdt(lwwSetKey);
clients.get(0).merge(generateSharedMsg(lwwSetKey, lww.remove("3")));
// assert deletion
assertMerged(clients.get(0), lwwSetKey, new OrSet<>("1", "2", "4").value());
assertMerged(clients.get(1), lwwSetKey, new OrSet<>("1", "2", "4").value());
@Test
public void OrSetTest(){
crdtSetTest("cror", OrSet::new);
}
private void testGrowOnlyCounter(List<GossipManager> clients) {
givenDifferentIncrement(clients);
assertThatCountIsUpdated(clients, 3);
givenIncreaseOther(clients);
assertThatCountIsUpdated(clients, 7);
@Test
public void LWWSetTest(){
crdtSetTest("crlww", LwwSet::new);
}
private void testPNCounter(List<GossipManager> clients) {
givenPNCounter(clients);
assertThatPNCounterSettlesAt(clients, 0);
int[] delta1 = { 2, 3 };
givenPNCounterUpdate(clients, delta1);
assertThatPNCounterSettlesAt(clients, 5);
int[] delta2 = { -3, 5 };
givenPNCounterUpdate(clients, delta2);
assertThatPNCounterSettlesAt(clients, 7);
int[] delta3 = { 1, 1 };
givenPNCounterUpdate(clients, delta3);
assertThatPNCounterSettlesAt(clients, 9);
int[] delta4 = { 1, -7 };
givenPNCounterUpdate(clients, delta4);
assertThatPNCounterSettlesAt(clients, 3);
@Test
public void MaxChangeSetTest(){
crdtSetTest("crmcs", MaxChangeSet::new);
}
private void givenDifferentIncrement(final List<GossipManager> clients) {
@Test
public void GrowOnlyCounterTest(){
Consumer<Long> assertCountUpdated = count -> {
for (GossipManager client : clients){
TUnit.assertThat(() -> client.findCrdt(gCounterKey))
.afterWaitingAtMost(10, TimeUnit.SECONDS)
.isEqualTo(new GrowOnlyCounter(new GrowOnlyCounter.Builder(client).increment(count)));
}
};
//generate different increment
Object payload = new GrowOnlyCounter(new GrowOnlyCounter.Builder(clients.get(0)).increment(1L));
clients.get(0).merge(generateSharedMsg(gCounterKey, payload));
payload = new GrowOnlyCounter(new GrowOnlyCounter.Builder(clients.get(1)).increment(2L));
clients.get(1).merge(generateSharedMsg(gCounterKey, payload));
}
private void givenIncreaseOther(final List<GossipManager> clients) {
assertCountUpdated.accept((long) 3);
//update one
GrowOnlyCounter gc = (GrowOnlyCounter) clients.get(1).findCrdt(gCounterKey);
GrowOnlyCounter gc2 = new GrowOnlyCounter(gc,
new GrowOnlyCounter.Builder(clients.get(1)).increment(4L));
new GrowOnlyCounter.Builder(clients.get(1)).increment(4L));
clients.get(1).merge(generateSharedMsg(gCounterKey, gc2));
assertCountUpdated.accept((long) 7);
}
private void assertMerged(final GossipManager client, String key, final Set<String> expected) {
TUnit.assertThat(() -> client.findCrdt(key).value()).afterWaitingAtMost(10, TimeUnit.SECONDS)
.isEqualTo(expected);
}
@Test
public void PNCounterTest(){
Consumer<List<Integer>> counterUpdate = list -> {
int clientIndex = 0;
for (int delta : list){
PNCounter c = (PNCounter) clients.get(clientIndex).findCrdt(pnCounterKey);
c = new PNCounter(c, new PNCounter.Builder(clients.get(clientIndex)).increment(((long) delta)));
clients.get(clientIndex).merge(generateSharedMsg(pnCounterKey, c));
clientIndex = (clientIndex + 1) % clients.size();
}
};
private void givenDifferentDatumsInSet(final List<GossipManager> clients) {
clients.get(0).merge(CrdtMessage("1"));
clients.get(1).merge(CrdtMessage("2"));
}
// given PNCounter
clients.get(0).merge(generateSharedMsg(pnCounterKey, new PNCounter(new PNCounter.Builder(clients.get(0)))));
clients.get(1).merge(generateSharedMsg(pnCounterKey, new PNCounter(new PNCounter.Builder(clients.get(1)))));
private void assertThatCountIsUpdated(final List<GossipManager> clients, long finalCount) {
TUnit.assertThat(() -> clients.get(0).findCrdt(gCounterKey))
.afterWaitingAtMost(10, TimeUnit.SECONDS).isEqualTo(new GrowOnlyCounter(
new GrowOnlyCounter.Builder(clients.get(0)).increment(finalCount)));
}
assertMergedCrdt(pnCounterKey, (long) 0);
private void assertThatListIsMerged(final List<GossipManager> clients) {
TUnit.assertThat(() -> clients.get(0).findCrdt("cr")).afterWaitingAtMost(10, TimeUnit.SECONDS)
.isEqualTo(new GrowOnlySet<>(Arrays.asList("1", "2")));
}
private void givenPNCounter(List<GossipManager> clients) {
{
SharedDataMessage d = new SharedDataMessage();
d.setKey(pnCounterKey);
d.setPayload(new PNCounter(new PNCounter.Builder(clients.get(0))));
d.setExpireAt(Long.MAX_VALUE);
d.setTimestamp(System.currentTimeMillis());
clients.get(0).merge(d);
}
{
SharedDataMessage d = new SharedDataMessage();
d.setKey(pnCounterKey);
d.setPayload(new PNCounter(new PNCounter.Builder(clients.get(1))));
d.setExpireAt(Long.MAX_VALUE);
d.setTimestamp(System.currentTimeMillis());
clients.get(1).merge(d);
List<List<Integer>> updateLists = new ArrayList<>();
updateLists.add(Arrays.asList(2, 3));
updateLists.add(Arrays.asList(-3, 5));
updateLists.add(Arrays.asList(1, 1));
updateLists.add(Arrays.asList(1, -7));
Long[] expectedResults = {5L, 7L, 9L, 3L};
for (int i = 0; i < updateLists.size(); i++){
counterUpdate.accept(updateLists.get(i));
assertMergedCrdt(pnCounterKey, expectedResults[i]);
}
}
private void givenPNCounterUpdate(List<GossipManager> clients, int[] deltaArray) {
int clientIndex = 0;
for (int delta: deltaArray) {
PNCounter c = (PNCounter) clients.get(clientIndex).findCrdt(pnCounterKey);
c = new PNCounter(c, new PNCounter.Builder(clients.get(clientIndex)).increment(((long)delta)));
SharedDataMessage d = new SharedDataMessage();
d.setKey(pnCounterKey);
d.setPayload(c);
d.setExpireAt(Long.MAX_VALUE);
d.setTimestamp(System.currentTimeMillis());
clients.get(clientIndex).merge(d);
clientIndex = (clientIndex + 1) % clients.size();
@Test
public void GrowOnlySetTest(){
clients.get(0).merge(generateSharedMsg("cr", new GrowOnlySet<>(Arrays.asList("1"))));
clients.get(1).merge(generateSharedMsg("cr", new GrowOnlySet<>(Arrays.asList("2"))));
assertMergedCrdt("cr", new GrowOnlySet<>(Arrays.asList("1", "2")).value());
}
private void assertMergedCrdt(String key, Object expected){
for (GossipManager client : clients){
TUnit.assertThat(() -> client.findCrdt(key).value())
.afterWaitingAtMost(10, TimeUnit.SECONDS).isEqualTo(expected);
}
}
private void assertThatPNCounterSettlesAt(List<GossipManager> clients, long expectedValue) {
for (GossipManager client: clients) {
TUnit.assertThat(() -> {
long value = 0;
Object o = client.findCrdt(pnCounterKey);
if (o != null) {
PNCounter c = (PNCounter)o;
value = c.value();
}
return value;
}).afterWaitingAtMost(10, TimeUnit.SECONDS)
.isEqualTo(expectedValue);
}
}
private SharedDataMessage CrdtMessage(String item) {
return generateSharedMsg("cr", new GrowOnlySet<>(Arrays.asList(item)));
}
private PerNodeDataMessage generatePerNodeMsg(String key, Object payload) {
private PerNodeDataMessage generatePerNodeMsg(String key, Object payload){
PerNodeDataMessage g = new PerNodeDataMessage();
g.setExpireAt(Long.MAX_VALUE);
g.setKey(key);
@ -274,7 +229,7 @@ public class DataTest extends AbstractIntegrationBase {
return g;
}
private SharedDataMessage generateSharedMsg(String key, Object payload) {
private SharedDataMessage generateSharedMsg(String key, Object payload){
SharedDataMessage d = new SharedDataMessage();
d.setKey(key);
d.setPayload(payload);

View File

@ -22,7 +22,8 @@ import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.gossip.GossipSettings;
import org.apache.gossip.Member;
import org.apache.gossip.crdt.LWWSet;
import org.apache.gossip.crdt.LwwSet;
import org.apache.gossip.crdt.MaxChangeSet;
import org.apache.gossip.crdt.OrSet;
import org.apache.gossip.manager.GossipManager;
import org.apache.gossip.manager.GossipManagerBuilder;
@ -39,7 +40,7 @@ import java.util.List;
import java.util.UUID;
public class JacksonTest {
private static GossipSettings simpleSettings(GossipSettings settings) {
settings.setPersistRingState(false);
settings.setPersistDataState(false);
@ -47,12 +48,12 @@ public class JacksonTest {
settings.setProtocolManagerClass("org.apache.gossip.protocol.json.JacksonProtocolManager");
return settings;
}
private static GossipSettings withSigning(GossipSettings settings) {
settings.setSignMessages(true);
return settings;
}
// formerly of SignedMessageTest.
@Test(expected = IllegalArgumentException.class)
public void ifSignMustHaveKeys()
@ -70,11 +71,11 @@ public class JacksonTest {
.build();
gossipService.init();
}
@Test
public void jacksonSerialTest() throws InterruptedException, URISyntaxException, IOException {
ObjectMapper objectMapper = JacksonProtocolManager.buildObjectMapper(simpleSettings(new GossipSettings()));
OrSet<Integer> i = new OrSet<Integer>(new OrSet.Builder<Integer>().add(1).remove(1));
String s = objectMapper.writeValueAsString(i);
@SuppressWarnings("unchecked")
@ -82,38 +83,50 @@ public class JacksonTest {
Assert.assertEquals(back, i);
}
@Test
public void jacksonCrdtLWWSetTest() {
void jacksonCrdtSeDeTest(Object value, Class<?> cl){
ObjectMapper objectMapper = JacksonProtocolManager.buildObjectMapper(simpleSettings(new GossipSettings()));
LWWSet<String> lww = new LWWSet<>("a", "b", "c");
try {
String lwwS = objectMapper.writeValueAsString(lww);
String valueS = objectMapper.writeValueAsString(value);
@SuppressWarnings("unchecked")
LWWSet<String> parsedLww = objectMapper.readValue(lwwS, LWWSet.class);
Assert.assertEquals(lww, parsedLww);
Object parsedValue = objectMapper.readValue(valueS, cl);
Assert.assertEquals(value, parsedValue);
} catch (Exception e) {
Assert.fail("LWWSet se/de error");
Assert.fail("Jackson se/de error");
}
}
@Test
public void jacksonOrSetTest(){
jacksonCrdtSeDeTest(new OrSet<>("1", "2", "3"), OrSet.class);
}
@Test
public void jacksonLWWSetTest(){
jacksonCrdtSeDeTest(new LwwSet<>("1", "2", "3"), LwwSet.class);
}
@Test
public void jacksonMaxChangeSetTest(){
jacksonCrdtSeDeTest(new MaxChangeSet<>("1", "2", "3"), MaxChangeSet.class);
}
@Test
public void testMessageEqualityAssumptions() {
long timeA = System.nanoTime();
long timeB = System.nanoTime();
Assert.assertNotEquals(timeA, timeB);
TestMessage messageA0 = new TestMessage(Long.toHexString(timeA));
TestMessage messageA1 = new TestMessage(Long.toHexString(timeA));
TestMessage messageB = new TestMessage(Long.toHexString(timeB));
Assert.assertEquals(messageA0, messageA1);
Assert.assertFalse(messageA0 == messageA1);
Assert.assertNotEquals(messageA0, messageB);
Assert.assertNotEquals(messageA1, messageB);
}
// ideally, we would test the serializability of every message type, but we just want to make sure this works in
// basic cases.
@Test