read(String key) {
+ return fromNullable(storeStateMachine.get(key));
+ }
+
+
+}
diff --git a/barge-store/src/main/java/org/robotninjas/barge/store/RaftStoreServer.java b/barge-store/src/main/java/org/robotninjas/barge/store/RaftStoreServer.java
new file mode 100644
index 0000000..fe273fa
--- /dev/null
+++ b/barge-store/src/main/java/org/robotninjas/barge/store/RaftStoreServer.java
@@ -0,0 +1,158 @@
+package org.robotninjas.barge.store;
+
+import org.glassfish.hk2.utilities.Binder;
+import org.glassfish.hk2.utilities.binding.AbstractBinder;
+
+import org.robotninjas.barge.jaxrs.Jackson;
+import org.robotninjas.barge.jaxrs.RaftApplication;
+import org.robotninjas.barge.jaxrs.ws.RaftJettyServer;
+
+import org.slf4j.bridge.SLF4JBridgeHandler;
+
+import java.io.File;
+import java.io.IOException;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import java.util.logging.Level;
+
+import javax.inject.Singleton;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+
+
+/**
+ * A standalone store accepting put/get operations through HTTP requests.
+ *
+ * This is also the main class for the barge-store application. A {@link org.robotninjas.barge.store.RaftStoreServer} instnace
+ * encapsulates an instance of {@link org.robotninjas.barge.jaxrs.ws.RaftJettyServer} which holds the main Jersey resources
+ * context. This context is extended with a REST resource {@link org.robotninjas.barge.store.StoreResource} mapped to the URI
+ * /raft/store/* , a {@link org.robotninjas.barge.store.RaftStore} implementation backed by a {@link org.robotninjas.barge.store.StoreStateMachine}
+ * which holds the result of Barge's cluster operations.
+ *
+ * Within this implementation, writes are always sent to the cluster and reads always done against the current backing
+ * state machine, implying that only committed read can ever be seen.
+ */
+public class RaftStoreServer {
+
+ private static final String help = "Usage: java -jar barge-store.jar [options] \n" +
+ "Options:\n" +
+ " -h : Displays this help message\n" +
+ " -c : Use given configuration file for cluster configuration\n" +
+ " This file is a simple property file with indices as keys and URIs as values, eg. like\n\n" +
+ " 0=http://localhost:1234\n" +
+ " 1=http://localhost:3456\n" +
+ " 2=http://localhost:4567\n\n" +
+ " Default is './barge.conf'\n" +
+ " : Index of this server in the cluster configuration\n";
+
+ private static final ClusterConfiguration cluster = new ClusterConfiguration();
+
+ private final int serverIndex;
+ private final URI[] clusterURIs;
+ private final File logDir;
+ private RaftJettyServer server;
+
+ public RaftStoreServer(int serverIndex, URI[] clusterURIs, File logDir) {
+ this.serverIndex = serverIndex;
+ this.clusterURIs = clusterURIs;
+ this.logDir = logDir;
+ }
+
+
+ public static void main(String[] args) throws IOException, URISyntaxException {
+ muteJul();
+
+ File clusterConfiguration = new File("barge.conf");
+ int index = -1;
+
+ for (int i = 0; i < args.length; i++) {
+
+ switch (args[i]) {
+
+ case "-c":
+ clusterConfiguration = new File(args[++i]);
+
+ break;
+
+ case "-h":
+ usage();
+ System.exit(0);
+
+ default:
+
+ try {
+ index = Integer.parseInt(args[i].trim());
+ } catch (NumberFormatException e) {
+ usage();
+ System.exit(1);
+ }
+
+ break;
+ }
+ }
+
+ if (index == -1) {
+ usage();
+ System.exit(1);
+ }
+
+ URI[] uris = cluster.readConfiguration(clusterConfiguration);
+
+ final File logDir = new File("log" + index);
+
+ RaftStoreServer server = new RaftStoreServer(index, uris, logDir);
+
+ server.start(uris[index].getPort());
+ }
+
+ private static void muteJul() {
+ java.util.logging.Logger.getLogger("").setLevel(Level.ALL);
+ SLF4JBridgeHandler.removeHandlersForRootLogger();
+ SLF4JBridgeHandler.install();
+ }
+
+ private static void usage() {
+ System.out.println(help);
+ }
+
+ public void start(int port) {
+ final StoreStateMachine stateMachine = new StoreStateMachine();
+
+ // Bridges our store objects to the DI context of the main RaftApplication context and Guice modules
+ Binder binder = new AbstractBinder() {
+ @Override
+ protected void configure() {
+ bind(stateMachine).to(StoreStateMachine.class);
+ bind(RaftStoreInstance.class).to(RaftStore.class).in(Singleton.class);
+ }
+ };
+
+ server =
+ new RaftJettyServer.Builder().setApplicationBuilder(new RaftApplication.Builder() //
+ .setServerIndex(serverIndex) //
+ .setUris(clusterURIs) //
+ .setLogDir(logDir) //
+ .register(StoreResource.class) //
+ .registerInstance(binder) //
+ .setStateMachine(stateMachine)) //
+ .build();
+
+ server.start(port);
+
+ // automatically call init on the registered raft instance
+ init();
+ }
+
+ private void init() {
+ final Client client = ClientBuilder.newBuilder().register(Jackson.customJacksonProvider()).build();
+ client.target(clusterURIs[serverIndex]).path("/raft/init").request().post(Entity.json(""));
+ }
+
+ public void stop() {
+ server.stop();
+ }
+}
diff --git a/barge-store/src/main/java/org/robotninjas/barge/store/StoreResource.java b/barge-store/src/main/java/org/robotninjas/barge/store/StoreResource.java
new file mode 100644
index 0000000..74bebe7
--- /dev/null
+++ b/barge-store/src/main/java/org/robotninjas/barge/store/StoreResource.java
@@ -0,0 +1,49 @@
+package org.robotninjas.barge.store;
+
+import com.google.common.base.Optional;
+
+import javax.inject.Inject;
+
+import javax.ws.rs.*;
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
+import javax.ws.rs.core.Response;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.ok;
+import javax.ws.rs.core.UriInfo;
+
+
+@Path("/store")
+public class StoreResource {
+
+ private RaftStore raftStore;
+
+ @Inject
+ private UriInfo uriInfo;
+
+ @Inject
+ public StoreResource(RaftStore raftStore) {
+ this.raftStore = raftStore;
+ }
+
+ @Path("{key:.+}")
+ @PUT
+ @Consumes(APPLICATION_OCTET_STREAM)
+ public Response store(@PathParam("key") String key, byte[] value) {
+ byte[] bytes = raftStore.write(new Write(key, value));
+
+ return created(uriInfo.getRequestUri()).type(APPLICATION_OCTET_STREAM).entity(bytes).build();
+ }
+
+ @Path("{key:.+}")
+ @GET
+ @Produces(APPLICATION_OCTET_STREAM)
+ public Response read(@PathParam("key") String key) {
+ Optional bytes = raftStore.read(key);
+
+ if (bytes.isPresent()) {
+ return ok(bytes.get()).type(APPLICATION_OCTET_STREAM).build();
+ }
+
+ throw new NotFoundException("key " + key + " is not mapped in this store");
+ }
+}
diff --git a/barge-store/src/main/java/org/robotninjas/barge/store/StoreStateMachine.java b/barge-store/src/main/java/org/robotninjas/barge/store/StoreStateMachine.java
new file mode 100644
index 0000000..551064c
--- /dev/null
+++ b/barge-store/src/main/java/org/robotninjas/barge/store/StoreStateMachine.java
@@ -0,0 +1,32 @@
+package org.robotninjas.barge.store;
+
+import com.google.common.collect.Maps;
+
+import org.robotninjas.barge.StateMachine;
+
+import java.nio.ByteBuffer;
+
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+
+/**
+ */
+public class StoreStateMachine implements StateMachine {
+
+ private final OperationsSerializer operationsSerializer = new OperationsSerializer();
+ private final Map store = Maps.newConcurrentMap();
+
+ @Override
+ public Object applyOperation(@Nonnull ByteBuffer entry) {
+ Write write = operationsSerializer.deserialize(entry);
+
+ return store.put(write.getKey(), write.getValue());
+ }
+
+
+ public byte[] get(String key) {
+ return store.get(key);
+ }
+}
diff --git a/barge-store/src/main/java/org/robotninjas/barge/store/Write.java b/barge-store/src/main/java/org/robotninjas/barge/store/Write.java
new file mode 100644
index 0000000..444e1c7
--- /dev/null
+++ b/barge-store/src/main/java/org/robotninjas/barge/store/Write.java
@@ -0,0 +1,61 @@
+package org.robotninjas.barge.store;
+
+import static com.google.common.base.Objects.toStringHelper;
+
+import java.io.Serializable;
+
+import java.util.Arrays;
+import java.util.Objects;
+import static java.util.Objects.hash;
+
+
+/**
+ * A write operation to the underlying store.
+ * Writes a value for key .
+ */
+public class Write implements Serializable {
+
+ private final String key;
+ private final byte[] value;
+
+ public Write(String key, byte[] value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ @SuppressWarnings("UnusedDeclaration")
+ public String getKey() {
+ return key;
+ }
+
+ @SuppressWarnings("UnusedDeclaration")
+ public byte[] getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this).add("key", key).add("value", value).toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return hash(key, value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+
+ if (this == obj) {
+ return true;
+ }
+
+ if ((obj == null) || (getClass() != obj.getClass())) {
+ return false;
+ }
+
+ final Write other = (Write) obj;
+
+ return Objects.equals(this.key, other.key) && Arrays.equals(this.value, other.value);
+ }
+}
diff --git a/barge-store/src/main/java/start.java b/barge-store/src/main/java/start.java
new file mode 100644
index 0000000..1ef419d
--- /dev/null
+++ b/barge-store/src/main/java/start.java
@@ -0,0 +1,39 @@
+import com.google.common.collect.Lists;
+
+import org.robotninjas.barge.store.RaftStoreServer;
+
+import java.io.File;
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+
+/**
+ * Wrapper over {@link org.robotninjas.barge.store.RaftStoreServer} that forks a new instance in the background.
+ *
+ * This main class is used to run barge in the background without relying on native mechanism: It forks actual main to another
+ * process and redirects stdout and stderr to some files.
+ *
+ */
+public class start {
+
+ private static final String JAVA = System.getProperty("java.home", "java");
+ private static final String CLASSPATH = System.getProperty("java.class.path", "barge-store.jar");
+
+ public static void main(String[] args) throws IOException {
+ ArrayList arguments = Lists.newArrayList(JAVA + "/bin/java", "-cp", CLASSPATH, RaftStoreServer.class.getName());
+
+ Collections.addAll(arguments, args);
+
+ long timestamp = System.currentTimeMillis();
+ File stdout = new File(".barge." + timestamp + ".out");
+ File stderr = new File(".barge." + timestamp + ".err");
+
+ ProcessBuilder builder = new ProcessBuilder(arguments).redirectError(stderr)
+ .redirectOutput(stdout)
+ .redirectInput(ProcessBuilder.Redirect.PIPE);
+
+ builder.start();
+ }
+}
diff --git a/barge-store/src/main/java/stop.java b/barge-store/src/main/java/stop.java
new file mode 100644
index 0000000..b3272ca
--- /dev/null
+++ b/barge-store/src/main/java/stop.java
@@ -0,0 +1,13 @@
+/**
+ * Main class to stop a running barge instance from the command-line.
+ *
+ * Needs a barge.conf file. If given one or more instance names, it will shutdown only those instances, otherwise
+ * it shuts down all instances fron the configuration file.
+ *
+ */
+public class stop {
+
+ public static void main(String[] args) {
+
+ }
+}
diff --git a/barge-store/src/test/java/org/robotninjas/barge/store/ControlResourceTest.java b/barge-store/src/test/java/org/robotninjas/barge/store/ControlResourceTest.java
new file mode 100644
index 0000000..be7c789
--- /dev/null
+++ b/barge-store/src/test/java/org/robotninjas/barge/store/ControlResourceTest.java
@@ -0,0 +1,48 @@
+package org.robotninjas.barge.store;
+
+import com.google.common.collect.Sets;
+
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.util.Set;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+
+
+public class ControlResourceTest extends JerseyTest {
+
+ private Control control;
+
+ @Override
+ protected Application configure() {
+ control = mock(Control.class);
+
+ ResourceConfig resourceConfig = ResourceConfig.forApplication(new Application() {
+ @Override
+ public Set getSingletons() {
+ return Sets.newHashSet((Object) new ControlResource(control));
+ }
+ });
+
+ resourceConfig.register(OperationsSerializer.jacksonModule());
+
+ return resourceConfig;
+ }
+
+
+ @Test
+ public void onPOSTStopWithEmptyContentRequestsShutdownOfVM() throws Exception {
+ client().target("/control/stop").request().post(Entity.entity("", MediaType.TEXT_PLAIN));
+
+ verify(control).shutdown();
+ }
+
+}
diff --git a/barge-store/src/test/java/org/robotninjas/barge/store/RaftStoreInstanceTest.java b/barge-store/src/test/java/org/robotninjas/barge/store/RaftStoreInstanceTest.java
new file mode 100644
index 0000000..2300a2b
--- /dev/null
+++ b/barge-store/src/test/java/org/robotninjas/barge/store/RaftStoreInstanceTest.java
@@ -0,0 +1,42 @@
+package org.robotninjas.barge.store;
+
+import com.google.common.util.concurrent.Futures;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.assertj.guava.api.Assertions;
+
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.robotninjas.barge.state.Raft;
+
+
+public class RaftStoreInstanceTest {
+
+ private static final byte[] oldValue = new byte[] { 0x41 };
+ private static final Write write = new Write("foo", new byte[] { 0x42 });
+
+ private final Raft raft = mock(Raft.class);
+ private final StoreStateMachine storeStateMachine = mock(StoreStateMachine.class);
+
+ private final OperationsSerializer serializer = new OperationsSerializer();
+
+ private final RaftStoreInstance raftStoreInstance = new RaftStoreInstance(raft, storeStateMachine);
+
+ @Test
+ public void commitSerializedWriteOperationToRaftThenReturnOldValueWhenWritingKeyValuePair() throws Exception {
+ when(raft.commitOperation(serializer.serialize(write))).thenReturn(Futures.immediateFuture(Futures.immediateFuture(oldValue)));
+
+ assertThat(raftStoreInstance.write(write)).isEqualTo(oldValue);
+ }
+
+ @Test
+ public void whenReadingReturnsAbsentOptionalIfKeyIsNotPresentInStore() throws Exception {
+ when(storeStateMachine.get("foo")).thenReturn(null);
+
+ Assertions.assertThat(raftStoreInstance.read("foo")).isAbsent();
+ }
+}
diff --git a/barge-store/src/test/java/org/robotninjas/barge/store/StoreResourceTest.java b/barge-store/src/test/java/org/robotninjas/barge/store/StoreResourceTest.java
new file mode 100644
index 0000000..c1d1875
--- /dev/null
+++ b/barge-store/src/test/java/org/robotninjas/barge/store/StoreResourceTest.java
@@ -0,0 +1,83 @@
+package org.robotninjas.barge.store;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Sets;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.eclipse.jetty.http.HttpStatus;
+
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Set;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
+import javax.ws.rs.core.Response;
+
+
+/**
+ */
+public class StoreResourceTest extends JerseyTest {
+
+ private static final byte[] oldValue = { 0x41 };
+ private static final byte[] value = { 0x42 };
+
+ private static final Write write = new Write("foo", value);
+
+ private RaftStore raftStore;
+
+ @Test
+ public void onPUTReturns201WithLocationAndPreviousValueGivenRaftStoreCompletesSuccessfully() throws Exception {
+ when(raftStore.write(write)).thenReturn(oldValue);
+
+ Response response = client().target("/store/foo").request().put(Entity.entity(value, APPLICATION_OCTET_STREAM_TYPE));
+
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED_201);
+ assertThat(response.getHeaderString("Location")).isEqualTo("http://localhost:9998/store/foo");
+ assertThat(response.readEntity(byte[].class)).isEqualTo(oldValue);
+ }
+
+ @Test
+ public void onGETReturns200WithFoundValueGivenStoreContainsRequestedKey() throws Exception {
+ when(raftStore.read("foo")).thenReturn(Optional.of(value));
+
+ Response response = client().target("/store/foo").request().get(Response.class);
+
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200);
+ assertThat(response.readEntity(byte[].class)).isEqualTo(value);
+ }
+
+ @Test
+ public void onGETReturns404GivenStoreDoesNotContainRequestedKey() throws Exception {
+ when(raftStore.read("foo")).thenReturn(Optional.absent());
+
+ Response response = client().target("/store/foo").request().get(Response.class);
+
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND_404);
+ }
+
+ @Override
+ protected Application configure() {
+ raftStore = mock(RaftStore.class);
+
+ ResourceConfig resourceConfig = ResourceConfig.forApplication(new Application() {
+ @Override
+ public Set getSingletons() {
+ return Sets.newHashSet((Object) new StoreResource(raftStore));
+ }
+ });
+
+ resourceConfig.register(OperationsSerializer.jacksonModule());
+
+ return resourceConfig;
+ }
+
+}
diff --git a/barge-store/src/test/java/org/robotninjas/barge/store/StoreServerTest.java b/barge-store/src/test/java/org/robotninjas/barge/store/StoreServerTest.java
new file mode 100644
index 0000000..1a05ae0
--- /dev/null
+++ b/barge-store/src/test/java/org/robotninjas/barge/store/StoreServerTest.java
@@ -0,0 +1,68 @@
+package org.robotninjas.barge.store;
+
+import com.google.common.base.Throwables;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.robotninjas.barge.jaxrs.Leaders;
+
+import java.io.File;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import static javax.ws.rs.client.Entity.entity;
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
+
+
+/**
+ */
+public class StoreServerTest {
+
+ private RaftStoreServer raftStoreServer;
+ private URI[] clusterURIs;
+ private File logDir = new File(new File(StoreServerTest.class.getResource("/marker").getFile()).getParentFile(), "log" + System.nanoTime());
+
+ @Before
+ public void startServer() throws URISyntaxException {
+ clusterURIs = new URI[] { serverURI() };
+ raftStoreServer = new RaftStoreServer(0, clusterURIs, logDir);
+
+ raftStoreServer.start(56789);
+ }
+
+ @After
+ public void stopServer() {
+ raftStoreServer.stop();
+ }
+
+ @Test
+ public void canWriteAndReadValuesInAStoreInAClusterOf1() throws Exception {
+ final Client client = ClientBuilder.newBuilder().register(OperationsSerializer.jacksonModule()).build();
+
+ new Leaders(clusterURIs).waitForALeader(client, 10000);
+
+ client.target(serverURI()).path("/raft/store/foo").request().put(entity(new byte[] { 0x1 }, APPLICATION_OCTET_STREAM_TYPE));
+
+ client.target(serverURI()).path("/raft/store/bar").request().put(entity(new byte[] { 0x2 }, APPLICATION_OCTET_STREAM_TYPE));
+
+ byte[] bytes = client.target(serverURI()).path("/raft/store/foo").request().get(byte[].class);
+
+ assertThat(bytes).isEqualTo(new byte[] { 0x1 });
+ }
+
+ private URI serverURI() {
+
+ try {
+ return new URI("http://localhost:56789/");
+ } catch (URISyntaxException e) {
+ throw Throwables.propagate(e);
+ }
+ }
+}
diff --git a/barge-store/src/test/java/org/robotninjas/barge/store/StoreStateMachineTest.java b/barge-store/src/test/java/org/robotninjas/barge/store/StoreStateMachineTest.java
new file mode 100644
index 0000000..1647c49
--- /dev/null
+++ b/barge-store/src/test/java/org/robotninjas/barge/store/StoreStateMachineTest.java
@@ -0,0 +1,44 @@
+package org.robotninjas.barge.store;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+import static java.nio.ByteBuffer.wrap;
+
+
+public class StoreStateMachineTest {
+
+
+ private static final Write write = new Write("foo", new byte[] { 0x42 });
+
+ private final OperationsSerializer serializer = new OperationsSerializer();
+
+ private final StoreStateMachine raftStoreInstance = new StoreStateMachine();
+
+ @Test
+ public void mapsKeyToValueWhenApplyingWriteOperation() throws Exception {
+ raftStoreInstance.applyOperation(wrap(serializer.serialize(write)));
+
+ byte[] read = raftStoreInstance.get("foo");
+
+ assertThat(read).isEqualTo(new byte[] { 0x42 });
+ }
+
+ @Test
+ public void applyingWriteReturnsOldValueGivenKeyIsAlreadyMapped() throws Exception {
+ raftStoreInstance.applyOperation(wrap(serializer.serialize(write)));
+
+ byte[] old = (byte[]) raftStoreInstance.applyOperation(wrap(serializer.serialize(new Write("foo", new byte[] { 0x43 }))));
+
+ assertThat(old).isEqualTo(new byte[] { 0x42 });
+ }
+
+ @Test
+ public void applyingWriteReturnsNullValueGivenKeyIsNotMapped() throws Exception {
+ byte[] old = (byte[]) raftStoreInstance.applyOperation(wrap(serializer.serialize(new Write("foo", new byte[] { 0x43 }))));
+
+ assertThat(old).isNull();
+ }
+
+}
diff --git a/barge-store/src/test/resources/marker b/barge-store/src/test/resources/marker
new file mode 100644
index 0000000..ba69cfc
--- /dev/null
+++ b/barge-store/src/test/resources/marker
@@ -0,0 +1 @@
+# marker file
\ No newline at end of file
diff --git a/jepsen-tests/.lein-failures b/jepsen-tests/.lein-failures
new file mode 100644
index 0000000..ba3884e
--- /dev/null
+++ b/jepsen-tests/.lein-failures
@@ -0,0 +1 @@
+#{barge.jepsen.test}
\ No newline at end of file
diff --git a/jepsen-tests/.lein-repl-history b/jepsen-tests/.lein-repl-history
new file mode 100644
index 0000000..e69de29
diff --git a/jepsen-tests/.vagrant/machines/jepsen/virtualbox/action_provision b/jepsen-tests/.vagrant/machines/jepsen/virtualbox/action_provision
new file mode 100644
index 0000000..e421f69
--- /dev/null
+++ b/jepsen-tests/.vagrant/machines/jepsen/virtualbox/action_provision
@@ -0,0 +1 @@
+1.5:2b928f8c-6664-4c21-bb2c-20be8d47d9e8
\ No newline at end of file
diff --git a/jepsen-tests/.vagrant/machines/jepsen/virtualbox/action_set_name b/jepsen-tests/.vagrant/machines/jepsen/virtualbox/action_set_name
new file mode 100644
index 0000000..b436678
--- /dev/null
+++ b/jepsen-tests/.vagrant/machines/jepsen/virtualbox/action_set_name
@@ -0,0 +1 @@
+1405001111
\ No newline at end of file
diff --git a/jepsen-tests/.vagrant/machines/jepsen/virtualbox/id b/jepsen-tests/.vagrant/machines/jepsen/virtualbox/id
new file mode 100644
index 0000000..036ec97
--- /dev/null
+++ b/jepsen-tests/.vagrant/machines/jepsen/virtualbox/id
@@ -0,0 +1 @@
+2b928f8c-6664-4c21-bb2c-20be8d47d9e8
\ No newline at end of file
diff --git a/jepsen-tests/.vagrant/machines/jepsen/virtualbox/index_uuid b/jepsen-tests/.vagrant/machines/jepsen/virtualbox/index_uuid
new file mode 100644
index 0000000..4b0b883
--- /dev/null
+++ b/jepsen-tests/.vagrant/machines/jepsen/virtualbox/index_uuid
@@ -0,0 +1 @@
+7984abde430c4615827642116c41561e
\ No newline at end of file
diff --git a/jepsen-tests/.vagrant/machines/jepsen/virtualbox/synced_folders b/jepsen-tests/.vagrant/machines/jepsen/virtualbox/synced_folders
new file mode 100644
index 0000000..ed5af14
--- /dev/null
+++ b/jepsen-tests/.vagrant/machines/jepsen/virtualbox/synced_folders
@@ -0,0 +1 @@
+{"virtualbox":{"/vagrant":{"guestpath":"/vagrant","hostpath":"/Users/arnaud/projects/raft/barge/jepsen-tests"}}}
\ No newline at end of file
diff --git a/jepsen-tests/Vagrantfile b/jepsen-tests/Vagrantfile
new file mode 100644
index 0000000..1b2598c
--- /dev/null
+++ b/jepsen-tests/Vagrantfile
@@ -0,0 +1,26 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+# a debian testing box for testing barge's jepsen tests (lots of tests here...)
+
+VAGRANTFILE_API_VERSION = "2"
+
+Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
+
+ config.vm.box = "jessie"
+ config.vm.box_url = "https://dl.dropboxusercontent.com/u/2060057/package.box"
+
+ proxy = ENV['http_proxy']
+
+ # machine for running jepsen tests
+ config.vm.define :jepsen do |mxconsole|
+ mxconsole.vm.hostname = "barge-jepsen"
+
+ # VM has own network so that we can identify it
+ config.vm.network "private_network", ip: "192.168.43.2"
+
+ mxconsole.vm.provision :shell, :path => "setup.sh", :args => proxy
+ end
+
+end
+
+
diff --git a/jepsen-tests/project.clj b/jepsen-tests/project.clj
new file mode 100644
index 0000000..75ac8bd
--- /dev/null
+++ b/jepsen-tests/project.clj
@@ -0,0 +1,6 @@
+(defproject barge-jepsen "0.1.0-alpha2-SNAPSHOT"
+ :description "Jepsen (Call me Maybe) Tests for Barge's linearizability"
+ :dependencies [[org.clojure/clojure "1.6.0-beta1"]
+ [org.clojure/tools.logging "0.2.6"]
+ [jepsen/jepsen "0.0.3-SNAPSHOT"]
+ [org.robotninjas.barge/barge-store "0.1.0-alpha2-SNAPSHOT"]])
diff --git a/jepsen-tests/setup.sh b/jepsen-tests/setup.sh
new file mode 100644
index 0000000..ee62e98
--- /dev/null
+++ b/jepsen-tests/setup.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+# configure lxc and boxes for running tests
+# need to be run as sudo/root
+
+PROXY=$1
+
+if [ z$PROXY != "z" ]; then
+ export http_proxy=$PROXY
+ export https_proxy=$PROXY
+fi
+
+# basic packages
+apt-get update
+apt-get install -y lxc bridge-utils libvirt-bin debootstrap clusterssh dnsmasq git
+
+# install java
+# from http://www.webupd8.org/2014/03/how-to-install-oracle-java-8-in-debian.html
+echo "deb http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" | tee /etc/apt/sources.list.d/webupd8team-java.list
+echo "deb-src http://ppa.launchpad.net/webupd8team/java/ubuntu trusty main" | tee -a /etc/apt/sources.list.d/webupd8team-java.list
+apt-key adv --keyserver keyserver.ubuntu.com --recv-keys EEA14886
+apt-get update
+
+echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections
+apt-get install -y oracle-java8-installer
+
+function install_lein() {
+ lein=~vagrant/bin/lein
+ mkdir -p $(dirname $lein)
+ wget -O $lein https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein
+ chmod +x $lein
+ echo PATH=$(dirname $lein):$PATH >> ~/.bash_profile
+}
+
+install_lein
+
diff --git a/jepsen-tests/src/barge/jepsen/system.clj b/jepsen-tests/src/barge/jepsen/system.clj
new file mode 100644
index 0000000..bedf347
--- /dev/null
+++ b/jepsen-tests/src/barge/jepsen/system.clj
@@ -0,0 +1,87 @@
+(ns barge.jepsen.system
+ (:require [clojure.java.io :as io]
+ [clojure.string :as str]
+ [clojure.tools.logging :refer [info]]
+ [jepsen.client :as client]
+ [jepsen.control :as c]
+ [jepsen.control.net :as net]
+ [jepsen.db :as db]
+ [jepsen.os.debian :as debian]
+ [jepsen.util :refer [meh timeout]]))
+
+
+;; GAV coordinates and repository where to find barge
+(def ^:dynamic *barge-version* "0.1.0-alpha2")
+(def ^:dynamic *barge-groupid* "org.robotninjas.barge")
+(def ^:dynamic *barge-artifactid* "barge-store")
+
+(def ^:dynamic *barge-repository* "https://dl.dropboxusercontent.com/u/2060057/maven/")
+
+(defn barge-jar []
+ (str *barge-artifactid* "-" *barge-version* ".jar"))
+
+(defn barge-uri []
+ (str *barge-repository*
+ (str/replace *barge-groupid* "." "/") "/"
+ *barge-artifactid* "/"
+ *barge-version* "/"
+ (barge-jar)))
+
+(defn barge-cluster-config
+ "Output configuration of barge cluster in a format suitable for populating a barge.conf
+ file, eg:
+
+```
+n1=http://1.2.3.4:8081
+n2=http://1.2.3.5:8081
+n3=http://1.2.3.6:8081
+```
+
+ * nodes: A sequence of nodes namespace
+ * fn-node-uri: Function to retrieve the node's IP given its name
+ "
+ [nodes fn-node-ip]
+ (str/join "\n"
+ (map (fn [node]
+ (str node "=http://" (fn-node-ip node) ":56789/"))
+ nodes
+ )))
+
+(barge-cluster-config ["n1" "n2" "n3"] (fn [_] "http://1.2.3.5:8081") )
+
+(def db
+ (reify db/DB
+ (setup! [_ test node]
+ (c/su
+ (info node "installing barge-store")
+ (c/cd "/tmp"
+ (info node "installing java")
+ (c/exec :apt-get :install :-y :openjdk-7-jre-headless)
+
+ (info node "installing barge " *barge-version*)
+ (c/exec :wget :-c (barge-uri))
+
+ (info node "configuring barge")
+ (c/exec :echo
+ (barge-cluster-config (:nodes test) #(c/on % (net/local-ip)))
+ :> "barge.conf")
+
+ (info node "starting barge")
+ (c/exec :java :-jar (symbol (barge-jar)) node)
+
+ )))
+
+ (teardown! [_ test node]
+ (c/su
+ (info node "removing barge-store")
+ (c/exec :java :-cp (barge-jar) :stop node)
+
+ (c/exec :rm :-fr (str "log" node))
+ ))))
+
+
+(c/with-ssh
+ {:username "vagrant"
+ :password "vagrant"}
+ (c/on "192.168.43.2"
+ (db/setup! db {:nodes ["192.168.43.2"]} "192.168.43.2")))
diff --git a/jepsen-tests/test/barge/jepsen/test.clj b/jepsen-tests/test/barge/jepsen/test.clj
new file mode 100644
index 0000000..92f78e3
--- /dev/null
+++ b/jepsen-tests/test/barge/jepsen/test.clj
@@ -0,0 +1,58 @@
+(ns barge.jepsen.test
+ (:use barge.jepsen.system
+ jepsen.core
+ jepsen.tests
+ clojure.test
+ clojure.pprint)
+ (:require [clojure.string :as str]
+ [jepsen.util :as util]
+ [jepsen.os.debian :as debian]
+ [jepsen.checker :as checker]
+ [jepsen.checker.timeline :as timeline]
+ [jepsen.model :as model]
+ [jepsen.generator :as gen]
+ [jepsen.nemesis :as nemesis]
+ [jepsen.store :as store]
+ [jepsen.report :as report]))
+
+(def the-db
+ (atom nil))
+
+(deftest register-test
+ (let [test (run!
+ (assoc
+ noop-test
+ :name "barge"
+ :os debian/os
+ :db (atom-db the-db)
+ :client (atom-client the-db)
+ :model (model/set)
+ :checker (checker/compose {:html timeline/html
+ :set checker/set})
+ :nemesis (nemesis/partitioner nemesis/bridge)
+ :generator (gen/phases
+ (->> (range)
+ (map (fn [x] {:type :invoke
+ :f :add
+ :value x}))
+ gen/seq
+ (gen/stagger 1/10)
+ (gen/delay 1)
+ (gen/nemesis
+ (gen/seq
+ (cycle [(gen/sleep 60)
+ {:type :info :f :start}
+ (gen/sleep 300)
+ {:type :info :f :stop}])))
+ (gen/time-limit 600))
+ (gen/nemesis
+ (gen/once {:type :info :f :stop}))
+ (gen/clients
+ (gen/once {:type :invoke :f :read})))))]
+ (is (:valid? (:results test)))
+ (report/linearizability (:linear (:results test)))
+ (pprint (:results test))))
+
+
+(gen/ops test [1 2 3 4 5] range)
+
diff --git a/jepsen-tests/todo.org b/jepsen-tests/todo.org
new file mode 100644
index 0000000..59b87dc
--- /dev/null
+++ b/jepsen-tests/todo.org
@@ -0,0 +1,98 @@
+#+TODO: OPTION(o) BREAKDOWN(b) TODO(t) STARTED(s) WAITING(w) | DONE(d) CANCELED(c)
+#+TAGS: developing(d) meeting(m) operations (o) planning (p) design(e)
+
+* TODO run jepsen tests on current barge
+** Jepsen Overview
+ - [[file:~/projects/raft/jepsen/src/jepsen/core.clj][core.clj]] contains the main *run* function which is the entry point for each single test run. It takes care of setting up OS,
+ DB, launching clients in parallel, runnning generator and perform model checking at end of test
+ - [[file:~/projects/raft/jepsen/src/jepsen/model.clj][model.clj]] describes knossos model implementations for common case: Sets, CASRegisters. Each model implements single
+ function *step* that applies some operation on a "model", possibly detecting and generating inconsistencies. For example,
+ applying a *cas* operation to a CASRegister is consistent iff the old and new values are identical. Simplest model is Mutex
+ which may acquired/released putting it in a locked/unlocked state. All Models are immutable thus allowing easy generation of
+ possible worlds from history
+ - see [[file:~/projects/raft/jepsen/src/jepsen/client.clj][client.clj]] for the protocol to implement per client. Clients are single threaded and send operations to server nodes
+ - see [[file:~/projects/raft/jepsen/src/jepsen/db.clj][db.clj]] for protocol to implement to setup/teardown each "DB" instance, in our case each barge instance protecting access
+ to underlying data.
+ - We need to provide some simple DB implemented over raft, eg. basic K/V store
+ - [[file:~/projects/raft/jepsen/src/jepsen/generator.clj][generator.clj]] is used to construct the test rig, eg. the generator for events to affect the database, including [[file:~/projects/raft/jepsen/src/jepsen/nemesis.clj][nemesis.clj]]
+ which encapsulates the logic to create network partitions or outages (drop packets, slow down link...). Implementation of
+ generators is actually a monad, very reminiscent of what's done in Quickcheck: One can compose generators, introducing
+ nemesis, delays, mapping functions...
+ - test results and outputs are persisted to a [[file:~/projects/raft/jepsen/src/jepsen/store.clj][store.clj]], basically a local file containing serializable objects from the test
+ - [[file:~/projects/raft/jepsen/src/jepsen/checker.clj][checker.clj]] defines the properties we may want to check for a given test, like linearizability which is the main purpose of
+ knossos library. Some properties we want to test: queue, total queue, set... Checks that the property is verified for a given
+ history and a given model: The history is what's produced by the (concurrent and generated test) and the model is how we
+ assume the underlying DB should behave. Properties may be more or less complex to check and for example checking
+ linearizability is somewhere in the vicinity of factorial in the length of the history...
+ - [[file:~/projects/raft/jepsen/src/jepsen/os.clj][os.clj]] and [[file:~/projects/raft/jepsen/src/jepsen/os/debian.clj][debian.clj]] provides interface and implementation for setting up OSes to run tests. Test runner uses low-level
+ commands from [[file:~/projects/raft/jepsen/src/jepsen/control.clj][control.clj]] which provides among other things an evolved *exec* function to run arbitrary processes, a *ssh*
+ function to run stuff over ssh connection and nice wrappers and macros like su or cd.
+ - provides system implementations and corresponding tests for the following DBs/Models
+ - elasticsearch/*set*
+ - datomic/CAS register
+ - etcd/CAS client
+ - rabbitmq/mutex
+** Knossos
+ - provides the conceptual framework to model-check a history against a model, testing for linearizability.
+ - *linearizability* is a specific level of consistency a concurrent system can exhibit (and guarantee to client). In a
+ linearizable system all operations which are assumed to take some time, appear to take effect instantly at some point between
+ their initiation and termination and in an order consistent with the *program order*. For example, given location x having
+ initial value 0, the concurrent operations of thread A doing a->read/a<-read(0), and thread B doing a->write(1)/a<-write() are
+ linearizable, as well as a->read()/a<-read(1).
+ - Of course more insteresting things happen when several concurrent threads write to the same location
+ - So given a history and a model we want to check if there is a linearization that is *consistent* with a model which
+ theoretically entails generating all possible interleaving of concurrent operations consistent with program order, and
+ checking there is at least one such consistent interleaving
+ - knossos leverages the laziness of clojure
+** DONE build/run jepsen
+*** DONE update dependencies to latest released versions
+ - https://github.com/aphyr/knossos -> 0.2
+ - clojurewerkz/elastisch -> 2.0.0-rc1
+ - test do not compile due to dependency on datomic stuff
+** CANCELED setup VM with coreos
+ - coreOS might not be a good choice in the short term as jepsen implements only debian OS. Need to implement docker-based OS??
+*** DONE upgrade coreos-vagrant
+https://github.com/coreos/coreos-vagrant
+*** DONE upgrade vagrant -> 1.6.2
+ - don't forget to set http_proxy env variables...
+*** DONE setup a debian testing (jessie) Box
+ - installed http://saimei.acc.umu.se/cdimage/jessie_di_alpha_1/amd64/iso-cd/debian-jessie-DI-a1-amd64-netinst.iso in VBox
+ - used as guideline for configuring box http://blog.codeboutique.com/post/creating-debian-squeeze-box-for-vagrant
+ - created package.box -> ~700MB
+*** DONE configure lxc and test boxes
+ - based on lxc.md from jepsen sources
+ - write it as a script (possibly a .clj using what's provided by jepsen...)
+ - configured all 5 boxes, started them -> works fine
+*** DONE install lein and build project
+ - project can build with lein, still issues with runnning test
+*** DONE install iptables on lxc containers
+*** DONE understand how to customize test executions for ssh authent on lxc containers
+*** DONE fix elastic search test issue
+ERROR in (create-test) (rest.clj:64)
+Uncaught exception, not in assertion.
+expected: nil
+ actual: java.lang.ClassCastException: java.lang.String cannot be cast to clojurewerkz.elastisch.rest.Connection
+ at clojurewerkz.elastisch.rest$url_with_path.doInvoke (rest.clj:64)
+ clojure.lang.RestFn.invoke (RestFn.java:423)
+ clojurewerkz.elastisch.rest$index_url.invoke (rest.clj:68)
+ clojurewerkz.elastisch.rest.index$create.doInvoke (index.clj:48)
+ clojure.lang.RestFn.invoke (RestFn.java:490)
+ jepsen.system.elasticsearch.CreateSetClient/fn (elasticsearch.clj:118)
+ jepsen.system.elasticsearch.CreateSetClient.setup_BANG_ (elasticsearch.clj:116)
+ jepsen.core$run_case_BANG_$fn__5082.invoke (core.clj:232)
+ clojure.lang.AFn.applyToHelper (AFn.java:154)
+ clojure.lang.AFn.applyTo (AFn.java:144)
+ clojure.core$apply.invoke (core.clj:624)
+ jepsen.core$fcatch$wrapper__5003.doInvoke (core.clj:39)
+ clojure.lang.RestFn.invoke (RestFn.java:408)
+ clojure.core$pmap$fn__6328$fn__6329.invoke (core.clj:6463)
+ clojure.core$binding_conveyor_fn$fn__4145.invoke (core.clj:1910)
+ clojure.lang.AFn.call (AFn.java:18)
+ java.util.concurrent.FutureTask.run (FutureTask.java:266)
+ java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1142)
+ java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:617)
+ java.lang.Thread.run (Thread.java:745)
+
+** DONE setup 5 docker containers with debian testing
+** TODO write test adapter for barge
+
diff --git a/pom.xml b/pom.xml
index be7dc93..2528a2b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,6 +43,7 @@
barge-tools
barge-rpc-proto
barge-jax-rs
+ barge-store
@@ -65,6 +66,12 @@
${project.version}
+
+ org.robotninjas.barge
+ barge-jax-rs
+ ${project.version}
+
+
org.robotninjas
@@ -211,6 +218,13 @@
test
+
+ org.assertj
+ assertj-guava
+ 1.2.0
+ test
+
+