Previously know as [patriknw/akka-data-replication](https://github.com/patriknw/akka-data-replication), which was originally inspired by [jboner/akka-crdt](https://github.com/jboner/akka-crdt). The functionality is very similar to akka-data-replication 0.11. Here is a list of the most important changes: * The package name changed to `akka.cluster.ddata` * The extension was renamed to `DistributedData` * The keys changed from strings to classes with unique identifiers and type information of the data values, e.g. `ORSetKey[Int]("set2")` * The optional read consistency parameter was removed from the `Update` message. If you need to read from other replicas before performing the update you have to first send a `Get` message and then continue with the ``Update`` when the ``GetSuccess`` is received. * `BigInt` is used in `GCounter` and `PNCounter` instead of `Long` * Improvements of java api * Better documentation
411 lines
14 KiB
Java
411 lines
14 KiB
Java
/**
|
|
* Copyright (C) 2015 Typesafe Inc. <http://www.typesafe.com>
|
|
*/
|
|
package docs.ddata;
|
|
|
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
|
import static org.junit.Assert.assertEquals;
|
|
|
|
import com.typesafe.config.ConfigFactory;
|
|
import java.math.BigInteger;
|
|
import java.util.Arrays;
|
|
import java.util.HashSet;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import org.junit.AfterClass;
|
|
import org.junit.BeforeClass;
|
|
import org.junit.Test;
|
|
import scala.PartialFunction;
|
|
import scala.concurrent.duration.Duration;
|
|
import scala.runtime.BoxedUnit;
|
|
|
|
import akka.actor.ActorRef;
|
|
import akka.actor.ActorSystem;
|
|
import akka.cluster.Cluster;
|
|
import akka.cluster.ddata.DistributedData;
|
|
import akka.cluster.ddata.Flag;
|
|
import akka.cluster.ddata.FlagKey;
|
|
import akka.cluster.ddata.GSet;
|
|
import akka.cluster.ddata.GSetKey;
|
|
import akka.cluster.ddata.Key;
|
|
import akka.cluster.ddata.LWWRegister;
|
|
import akka.cluster.ddata.ORSet;
|
|
import akka.cluster.ddata.ORSetKey;
|
|
import akka.cluster.ddata.PNCounter;
|
|
import akka.cluster.ddata.PNCounterKey;
|
|
import akka.cluster.ddata.PNCounterMap;
|
|
import akka.cluster.ddata.Replicator;
|
|
import akka.cluster.ddata.Replicator.Changed;
|
|
import akka.cluster.ddata.Replicator.Delete;
|
|
import akka.cluster.ddata.Replicator.GetFailure;
|
|
import akka.cluster.ddata.Replicator.GetSuccess;
|
|
import akka.cluster.ddata.Replicator.NotFound;
|
|
import akka.cluster.ddata.Replicator.ReadAll;
|
|
import akka.cluster.ddata.Replicator.ReadConsistency;
|
|
import akka.cluster.ddata.Replicator.ReadFrom;
|
|
import akka.cluster.ddata.Replicator.ReadMajority;
|
|
import akka.cluster.ddata.Replicator.Subscribe;
|
|
import akka.cluster.ddata.Replicator.UpdateSuccess;
|
|
import akka.cluster.ddata.Replicator.UpdateTimeout;
|
|
import akka.cluster.ddata.Replicator.WriteAll;
|
|
import akka.cluster.ddata.Replicator.WriteConsistency;
|
|
import akka.cluster.ddata.Replicator.WriteMajority;
|
|
import akka.cluster.ddata.Replicator.WriteTo;
|
|
import akka.japi.pf.ReceiveBuilder;
|
|
import akka.testkit.JavaTestKit;
|
|
|
|
public class DistributedDataDocTest {
|
|
|
|
static ActorSystem system;
|
|
|
|
void receive(PartialFunction<Object, BoxedUnit> pf) {
|
|
}
|
|
|
|
JavaTestKit probe = new JavaTestKit(system);
|
|
|
|
ActorRef self() {
|
|
return probe.getRef();
|
|
}
|
|
|
|
ActorRef sender() {
|
|
return probe.getRef();
|
|
}
|
|
|
|
@BeforeClass
|
|
public static void setup() {
|
|
system = ActorSystem.create("DistributedDataDocTest",
|
|
ConfigFactory.parseString(DistributedDataDocSpec.config()));
|
|
}
|
|
|
|
@AfterClass
|
|
public static void tearDown() {
|
|
JavaTestKit.shutdownActorSystem(system);
|
|
system = null;
|
|
}
|
|
|
|
@Test
|
|
public void demonstrateUpdate() {
|
|
probe = new JavaTestKit(system);
|
|
|
|
//#update
|
|
final Cluster node = Cluster.get(system);
|
|
final ActorRef replicator = DistributedData.get(system).replicator();
|
|
|
|
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
|
final Key<GSet<String>> set1Key = GSetKey.create("set1");
|
|
final Key<ORSet<String>> set2Key = ORSetKey.create("set2");
|
|
final Key<Flag> activeFlagKey = FlagKey.create("active");
|
|
|
|
replicator.tell(new Replicator.Update<PNCounter>(counter1Key, PNCounter.create(),
|
|
Replicator.writeLocal(), curr -> curr.increment(node, 1)), self());
|
|
|
|
final WriteConsistency writeTo3 = new WriteTo(3, Duration.create(1, SECONDS));
|
|
replicator.tell(new Replicator.Update<GSet<String>>(set1Key, GSet.create(),
|
|
writeTo3, curr -> curr.add("hello")), self());
|
|
|
|
final WriteConsistency writeMajority =
|
|
new WriteMajority(Duration.create(5, SECONDS));
|
|
replicator.tell(new Replicator.Update<ORSet<String>>(set2Key, ORSet.create(),
|
|
writeMajority, curr -> curr.add(node, "hello")), self());
|
|
|
|
final WriteConsistency writeAll = new WriteAll(Duration.create(5, SECONDS));
|
|
replicator.tell(new Replicator.Update<Flag>(activeFlagKey, Flag.create(),
|
|
writeAll, curr -> curr.switchOn()), self());
|
|
//#update
|
|
|
|
probe.expectMsgClass(UpdateSuccess.class);
|
|
//#update-response1
|
|
receive(ReceiveBuilder.
|
|
match(UpdateSuccess.class, a -> a.key().equals(counter1Key), a -> {
|
|
// ok
|
|
}).build());
|
|
//#update-response1
|
|
|
|
//#update-response2
|
|
receive(ReceiveBuilder.
|
|
match(UpdateSuccess.class, a -> a.key().equals(set1Key), a -> {
|
|
// ok
|
|
}).
|
|
match(UpdateTimeout.class, a -> a.key().equals(set1Key), a -> {
|
|
// write to 3 nodes failed within 1.second
|
|
}).build());
|
|
//#update-response2
|
|
}
|
|
|
|
@Test
|
|
public void demonstrateUpdateWithRequestContext() {
|
|
probe = new JavaTestKit(system);
|
|
|
|
//#update-request-context
|
|
final Cluster node = Cluster.get(system);
|
|
final ActorRef replicator = DistributedData.get(system).replicator();
|
|
|
|
final WriteConsistency writeTwo = new WriteTo(2, Duration.create(3, SECONDS));
|
|
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
|
|
|
receive(ReceiveBuilder.
|
|
match(String.class, a -> a.equals("increment"), a -> {
|
|
// incoming command to increase the counter
|
|
Optional<Object> reqContext = Optional.of(sender());
|
|
Replicator.Update<PNCounter> upd = new Replicator.Update<PNCounter>(counter1Key,
|
|
PNCounter.create(), writeTwo, reqContext, curr -> curr.increment(node, 1));
|
|
replicator.tell(upd, self());
|
|
}).
|
|
|
|
match(UpdateSuccess.class, a -> a.key().equals(counter1Key), a -> {
|
|
ActorRef replyTo = (ActorRef) a.getRequest().get();
|
|
replyTo.tell("ack", self());
|
|
}).
|
|
|
|
match(UpdateTimeout.class, a -> a.key().equals(counter1Key), a -> {
|
|
ActorRef replyTo = (ActorRef) a.getRequest().get();
|
|
replyTo.tell("nack", self());
|
|
}).build());
|
|
|
|
//#update-request-context
|
|
}
|
|
|
|
@SuppressWarnings({ "unused", "unchecked" })
|
|
@Test
|
|
public void demonstrateGet() {
|
|
probe = new JavaTestKit(system);
|
|
|
|
//#get
|
|
final ActorRef replicator = DistributedData.get(system).replicator();
|
|
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
|
final Key<GSet<String>> set1Key = GSetKey.create("set1");
|
|
final Key<ORSet<String>> set2Key = ORSetKey.create("set2");
|
|
final Key<Flag> activeFlagKey = FlagKey.create("active");
|
|
|
|
replicator.tell(new Replicator.Get<PNCounter>(counter1Key,
|
|
Replicator.readLocal()), self());
|
|
|
|
final ReadConsistency readFrom3 = new ReadFrom(3, Duration.create(1, SECONDS));
|
|
replicator.tell(new Replicator.Get<GSet<String>>(set1Key,
|
|
readFrom3), self());
|
|
|
|
final ReadConsistency readMajority = new ReadMajority(Duration.create(5, SECONDS));
|
|
replicator.tell(new Replicator.Get<ORSet<String>>(set2Key,
|
|
readMajority), self());
|
|
|
|
final ReadConsistency readAll = new ReadAll(Duration.create(5, SECONDS));
|
|
replicator.tell(new Replicator.Get<Flag>(activeFlagKey,
|
|
readAll), self());
|
|
//#get
|
|
|
|
//#get-response1
|
|
receive(ReceiveBuilder.
|
|
match(GetSuccess.class, a -> a.key().equals(counter1Key), a -> {
|
|
GetSuccess<PNCounter> g = a;
|
|
BigInteger value = g.dataValue().getValue();
|
|
}).
|
|
match(NotFound.class, a -> a.key().equals(counter1Key), a -> {
|
|
// key counter1 does not exist
|
|
}).build());
|
|
//#get-response1
|
|
|
|
//#get-response2
|
|
receive(ReceiveBuilder.
|
|
match(GetSuccess.class, a -> a.key().equals(set1Key), a -> {
|
|
GetSuccess<GSet<String>> g = a;
|
|
Set<String> value = g.dataValue().getElements();
|
|
}).
|
|
match(GetFailure.class, a -> a.key().equals(set1Key), a -> {
|
|
// read from 3 nodes failed within 1.second
|
|
}).
|
|
match(NotFound.class, a -> a.key().equals(set1Key), a -> {
|
|
// key set1 does not exist
|
|
}).build());
|
|
//#get-response2
|
|
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
@Test
|
|
public void demonstrateGetWithRequestContext() {
|
|
probe = new JavaTestKit(system);
|
|
|
|
//#get-request-context
|
|
final ActorRef replicator = DistributedData.get(system).replicator();
|
|
final ReadConsistency readTwo = new ReadFrom(2, Duration.create(3, SECONDS));
|
|
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
|
|
|
receive(ReceiveBuilder.
|
|
match(String.class, a -> a.equals("get-count"), a -> {
|
|
// incoming request to retrieve current value of the counter
|
|
Optional<Object> reqContext = Optional.of(sender());
|
|
replicator.tell(new Replicator.Get<PNCounter>(counter1Key,
|
|
readTwo), self());
|
|
}).
|
|
|
|
match(GetSuccess.class, a -> a.key().equals(counter1Key), a -> {
|
|
ActorRef replyTo = (ActorRef) a.getRequest().get();
|
|
GetSuccess<PNCounter> g = a;
|
|
long value = g.dataValue().getValue().longValue();
|
|
replyTo.tell(value, self());
|
|
}).
|
|
|
|
match(GetFailure.class, a -> a.key().equals(counter1Key), a -> {
|
|
ActorRef replyTo = (ActorRef) a.getRequest().get();
|
|
replyTo.tell(-1L, self());
|
|
}).
|
|
|
|
match(NotFound.class, a -> a.key().equals(counter1Key), a -> {
|
|
ActorRef replyTo = (ActorRef) a.getRequest().get();
|
|
replyTo.tell(0L, self());
|
|
}).build());
|
|
//#get-request-context
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
abstract class MyActor {
|
|
//#subscribe
|
|
final ActorRef replicator = DistributedData.get(system).replicator();
|
|
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
|
|
|
BigInteger currentValue = BigInteger.valueOf(0);
|
|
|
|
public MyActor() {
|
|
receive(ReceiveBuilder.
|
|
match(Changed.class, a -> a.key().equals(counter1Key), a -> {
|
|
Changed<PNCounter> g = a;
|
|
currentValue = g.dataValue().getValue();
|
|
}).
|
|
|
|
match(String.class, a -> a.equals("get-count"), a -> {
|
|
// incoming request to retrieve current value of the counter
|
|
sender().tell(currentValue, sender());
|
|
}).build());
|
|
}
|
|
|
|
public void preStart() {
|
|
// subscribe to changes of the Counter1Key value
|
|
replicator.tell(new Subscribe<PNCounter>(counter1Key, self()), ActorRef.noSender());
|
|
}
|
|
|
|
//#subscribe
|
|
}
|
|
|
|
@Test
|
|
public void demonstrateDelete() {
|
|
probe = new JavaTestKit(system);
|
|
|
|
//#delete
|
|
final ActorRef replicator = DistributedData.get(system).replicator();
|
|
final Key<PNCounter> counter1Key = PNCounterKey.create("counter1");
|
|
final Key<ORSet<String>> set2Key = ORSetKey.create("set2");
|
|
|
|
replicator.tell(new Delete<PNCounter>(counter1Key,
|
|
Replicator.writeLocal()), self());
|
|
|
|
final WriteConsistency writeMajority =
|
|
new WriteMajority(Duration.create(5, SECONDS));
|
|
replicator.tell(new Delete<PNCounter>(counter1Key,
|
|
writeMajority), self());
|
|
//#delete
|
|
}
|
|
|
|
public void demonstratePNCounter() {
|
|
//#pncounter
|
|
final Cluster node = Cluster.get(system);
|
|
final PNCounter c0 = PNCounter.create();
|
|
final PNCounter c1 = c0.increment(node, 1);
|
|
final PNCounter c2 = c1.increment(node, 7);
|
|
final PNCounter c3 = c2.decrement(node, 2);
|
|
System.out.println(c3.value()); // 6
|
|
//#pncounter
|
|
}
|
|
|
|
public void demonstratePNCounterMap() {
|
|
//#pncountermap
|
|
final Cluster node = Cluster.get(system);
|
|
final PNCounterMap m0 = PNCounterMap.create();
|
|
final PNCounterMap m1 = m0.increment(node, "a", 7);
|
|
final PNCounterMap m2 = m1.decrement(node, "a", 2);
|
|
final PNCounterMap m3 = m2.increment(node, "b", 1);
|
|
System.out.println(m3.get("a")); // 5
|
|
System.out.println(m3.getEntries());
|
|
//#pncountermap
|
|
}
|
|
|
|
public void demonstrateGSet() {
|
|
//#gset
|
|
final GSet<String> s0 = GSet.create();
|
|
final GSet<String> s1 = s0.add("a");
|
|
final GSet<String> s2 = s1.add("b").add("c");
|
|
if (s2.contains("a"))
|
|
System.out.println(s2.getElements()); // a, b, c
|
|
//#gset
|
|
}
|
|
|
|
public void demonstrateORSet() {
|
|
//#orset
|
|
final Cluster node = Cluster.get(system);
|
|
final ORSet<String> s0 = ORSet.create();
|
|
final ORSet<String> s1 = s0.add(node, "a");
|
|
final ORSet<String> s2 = s1.add(node, "b");
|
|
final ORSet<String> s3 = s2.remove(node, "a");
|
|
System.out.println(s3.getElements()); // b
|
|
//#orset
|
|
}
|
|
|
|
public void demonstrateFlag() {
|
|
//#flag
|
|
final Flag f0 = Flag.create();
|
|
final Flag f1 = f0.switchOn();
|
|
System.out.println(f1.enabled());
|
|
//#flag
|
|
}
|
|
|
|
@Test
|
|
public void demonstrateLWWRegister() {
|
|
//#lwwregister
|
|
final Cluster node = Cluster.get(system);
|
|
final LWWRegister<String> r1 = LWWRegister.create(node, "Hello");
|
|
final LWWRegister<String> r2 = r1.withValue(node, "Hi");
|
|
System.out.println(r1.value() + " by " + r1.updatedBy() + " at " + r1.timestamp());
|
|
//#lwwregister
|
|
assertEquals("Hi", r2.value());
|
|
}
|
|
|
|
static
|
|
//#lwwregister-custom-clock
|
|
class Record {
|
|
public final int version;
|
|
public final String name;
|
|
public final String address;
|
|
|
|
public Record(int version, String name, String address) {
|
|
this.version = version;
|
|
this.name = name;
|
|
this.address = address;
|
|
}
|
|
}
|
|
|
|
//#lwwregister-custom-clock
|
|
|
|
public void demonstrateLWWRegisterWithCustomClock() {
|
|
//#lwwregister-custom-clock
|
|
|
|
final Cluster node = Cluster.get(system);
|
|
final LWWRegister.Clock<Record> recordClock = new LWWRegister.Clock<Record>() {
|
|
@Override
|
|
public long apply(long currentTimestamp, Record value) {
|
|
return value.version;
|
|
}
|
|
};
|
|
|
|
final Record record1 = new Record(1, "Alice", "Union Square");
|
|
final LWWRegister<Record> r1 = LWWRegister.create(node, record1);
|
|
|
|
final Record record2 = new Record(2, "Alice", "Madison Square");
|
|
final LWWRegister<Record> r2 = LWWRegister.create(node, record2);
|
|
|
|
final LWWRegister<Record> r3 = r1.merge(r2);
|
|
System.out.println(r3.value());
|
|
//#lwwregister-custom-clock
|
|
|
|
assertEquals("Madison Square", r3.value().address);
|
|
}
|
|
|
|
}
|