/*
* ModeShape (http://www.modeshape.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modeshape.schematic;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertNull;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.modeshape.schematic.document.Document;
import org.modeshape.schematic.document.EditableDocument;
import org.modeshape.schematic.document.Json;
import org.modeshape.schematic.document.ParsingException;
import org.modeshape.schematic.internal.document.BasicDocument;
/**
* Base class for the different {@link SchematicDb} implementation.
*
* @author Horia Chiorean (hchiorea@redhat.com)
*/
public abstract class AbstractSchematicDBTest {
protected static final Document DEFAULT_CONTENT ;
private static final String VALUE_FIELD = "value";
protected SchematicDb db;
protected boolean print = false;
static {
try {
DEFAULT_CONTENT = Json.read(AbstractSchematicDBTest.class.getClassLoader().getResourceAsStream("document.json"));
} catch (ParsingException e) {
throw new RuntimeException(e);
}
}
protected abstract SchematicDb getDb() throws Exception ;
@Before
public void before() throws Exception {
db = getDb();
db.start();
}
@After
public void after() throws Exception {
db.stop();
}
@Test
public void shouldGetAndPut() throws Exception {
List<SchematicEntry> dbEntries = randomEntries(3);
//simulate the start of a transaction
db.txStarted("0");
//write some entries without committing
dbEntries.forEach(dbEntry -> db.put(dbEntry.id(), dbEntry.content()));
Set<String> expectedIds = dbEntries.stream().map(SchematicEntry::id).collect(Collectors.toCollection(TreeSet::new));
// check that the same connection is used and the entries are still there
assertTrue(db.keys().containsAll(expectedIds));
// simulate a commit for the write
db.txCommitted("0");
// check that the entries are still there
assertTrue(db.keys().containsAll(expectedIds));
// check that for each entry the content is correctly stored
dbEntries.stream().forEach(entry -> assertEquals(entry.content(), db.getEntry(entry.id()).content()));
// update one of the documents and check the update is correct
SchematicEntry firstEntry = dbEntries.get(0);
String idToUpdate = firstEntry.id();
EditableDocument updatedDocument = firstEntry.content().edit(true);
updatedDocument.setNumber(VALUE_FIELD, 2);
//simulate a new transaction
db.txStarted("1");
db.get(idToUpdate);
db.put(idToUpdate, updatedDocument);
assertEquals(updatedDocument, db.getEntry(idToUpdate).content());
db.txCommitted("1");
assertEquals(updatedDocument, db.getEntry(idToUpdate).content());
}
@Test
public void shouldReadSchematicEntry() throws Exception {
SchematicEntry entry = writeSingleEntry();
SchematicEntry schematicEntry = db.getEntry(entry.id());
assertNotNull(schematicEntry);
assertTrue(db.containsKey(entry.id()));
}
@Test
public void shouldEditContentDirectly() throws Exception {
// test the editing of content for an existing entry
SchematicEntry entry = writeSingleEntry();
EditableDocument editableDocument = simulateTransaction(() -> db.editContent(entry.id(), false));
assertNotNull(editableDocument);
assertEquals(entry.content(), editableDocument);
simulateTransaction(() -> {
EditableDocument document = db.editContent(entry.id(), false);
document.setNumber(VALUE_FIELD, 2);
return null;
});
Document doc = db.getEntry(entry.id()).content();
assertEquals(2, (int) doc.getInteger(VALUE_FIELD));
// test the editing of content for a new entry which should be create
String newId = UUID.randomUUID().toString();
EditableDocument newDocument = simulateTransaction(() -> db.editContent(newId, true));
assertNotNull(newDocument);
// the content in the DB should be an empty schematic entry...
SchematicEntry schematicEntry = db.getEntry(newId);
assertEquals(newId, schematicEntry.id());
assertEquals(new BasicDocument(), schematicEntry.content());
// test the editing of a non-existing id without creating a new entry for it
newDocument = simulateTransaction(() -> db.editContent(UUID.randomUUID().toString(), false));
assertNull(newDocument);
}
@Test
public void shouldPutIfAbsent() throws Exception {
SchematicEntry entry = writeSingleEntry();
EditableDocument editableDocument = entry.content().edit(true);
editableDocument.setNumber(VALUE_FIELD, 100);
SchematicEntry updatedEntry = simulateTransaction(() -> db.putIfAbsent(entry.id(), entry.content()));
assertNotNull(updatedEntry);
assertEquals(1, (int) updatedEntry.content().getInteger(VALUE_FIELD));
SchematicEntry newEntry = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
assertNull(simulateTransaction(() -> db.putIfAbsent(newEntry.id(), newEntry.content())));
updatedEntry = db.getEntry(newEntry.id());
assertNotNull(updatedEntry);
}
@Test
public void shouldPutSchematicEntry() throws Exception {
SchematicEntry originalEntry = randomEntries(1).get(0);
simulateTransaction(() -> {
db.putEntry(originalEntry.source());
return null;
});
SchematicEntry actualEntry = db.getEntry(originalEntry.id());
assertNotNull(actualEntry);
assertEquals(originalEntry.getMetadata(), actualEntry.getMetadata());
assertEquals(originalEntry.content(), actualEntry.content());
assertEquals(DEFAULT_CONTENT, actualEntry.content());
}
@Test
public void shouldRemoveDocument() throws Exception {
SchematicEntry entry = writeSingleEntry();
simulateTransaction(() -> db.remove(entry.id()));
assertFalse(db.containsKey(entry.id()));
}
@Test
public void shouldRemoveAllDocuments() throws Exception {
int count = 3;
simulateTransaction(() -> {
randomEntries(count).forEach(entry -> db.put(entry.id(), entry.content()));
return null;
});
assertFalse(db.keys().isEmpty());
simulateTransaction(() -> {
db.removeAll();
return null;
});
assertTrue(db.keys().isEmpty());
}
@Test
public void shouldIsolateChangesWithinTransaction() throws Exception {
SchematicEntry entry1 = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
SchematicEntry entry2 = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
CyclicBarrier syncBarrier = new CyclicBarrier(2);
CompletableFuture<Void> thread1 = CompletableFuture.runAsync(() -> changeAndCommit(entry1, entry2, syncBarrier));
CompletableFuture<Void> thread2 = CompletableFuture.runAsync(() -> changeAndCommit(entry2, entry1, syncBarrier));
thread1.get(3, TimeUnit.SECONDS);
thread2.get(3, TimeUnit.SECONDS);
// both transactions should've removed the entries in the end
Assert.assertFalse(db.containsKey(entry1.id()));
Assert.assertFalse(db.containsKey(entry2.id()));
}
@Test
public void shouldRollbackChangesWithinTransaction() throws Exception {
SchematicEntry entry1 = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
SchematicEntry entry2 = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
CyclicBarrier syncBarrier = new CyclicBarrier(2);
CompletableFuture<Void> thread1 = CompletableFuture.runAsync(() -> changeAndRollback(entry1, entry2, syncBarrier));
CompletableFuture<Void> thread2 = CompletableFuture.runAsync(() -> changeAndRollback(entry2, entry1, syncBarrier));
thread1.get(2, TimeUnit.SECONDS);
thread2.get(2, TimeUnit.SECONDS);
// both transactions should've rolledback their original changes
Assert.assertEquals(entry1.content(), db.getEntry(entry1.id()).content());
Assert.assertEquals(entry2.content(), db.getEntry(entry2.id()).content());
}
@Test
public void shouldInsertAndUpdateEntriesConcurrentlyWithMultipleWriters() throws Exception {
int threadsCount = 100;
int entriesPerThread = 100;
ExecutorService executors = Executors.newFixedThreadPool(threadsCount);
print = false;
print("Starting the run of " + threadsCount + " threads with " + entriesPerThread + " insertions per thread...");
long startTime = System.nanoTime();
List<Future<List<String>>> results = IntStream.range(0, threadsCount)
.mapToObj(value -> insertMultipleEntries(entriesPerThread, executors))
.collect(Collectors.toList());
results.stream()
.map(future -> {
try {
return future.get(2, TimeUnit.MINUTES);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.flatMap(List::stream)
.forEach(id -> Assert.assertTrue(db.containsKey(id)));
long durationMillis = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
if (print) {
System.out.printf("Total duration to insert " + threadsCount * entriesPerThread + " entries : " + durationMillis / 1000d + " seconds");
}
}
protected CompletableFuture<List<String>> insertMultipleEntries(int entriesPerThread, ExecutorService executors) {
return CompletableFuture.supplyAsync(() -> {
if (print) {
System.out.println(Thread.currentThread().getName() + " inserting " + entriesPerThread + " entries...");
}
String txId = UUID.randomUUID().toString();
db.txStarted(txId);
List<String> ids = null;
try {
ids = randomEntries(entriesPerThread)
.stream()
.map(dbEntry -> {
db.put(dbEntry.id(), dbEntry.content());
return dbEntry.id();
})
.collect(Collectors.toList());
} catch (Exception e) {
throw new RuntimeException(e);
}
db.txCommitted(txId);
return ids;
}, executors);
}
private void changeAndRollback(SchematicEntry ourEntry, SchematicEntry otherEntry, CyclicBarrier syncBarrier) {
try {
String txId = UUID.randomUUID().toString();
// start a tx and write the first entry
db.txStarted(txId);
db.put(ourEntry.id(), ourEntry.content());
db.txCommitted(txId);
syncBarrier.await();
Document ourDocument = db.getEntry(ourEntry.id()).content();
Document otherDocument = db.getEntry(otherEntry.id()).content();
Assert.assertEquals(ourDocument, otherDocument);
// start a new tx, make some changes and rollback
txId = UUID.randomUUID().toString();
db.txStarted(txId);
db.put(ourEntry.id(), new BasicDocument());
// rollback the tx
db.txRolledback(txId);
syncBarrier.await();
// and check that the visible documents are unchanged
Assert.assertEquals(ourDocument, db.getEntry(ourEntry.id()).content());
Assert.assertEquals(otherDocument, db.getEntry(otherEntry.id()).content());
} catch (RuntimeException re) {
syncBarrier.reset();
throw re;
} catch (Throwable t) {
t.printStackTrace();
syncBarrier.reset();
throw new RuntimeException(t);
}
}
protected void changeAndCommit(SchematicEntry ourEntry, SchematicEntry otherEntry, CyclicBarrier syncBarrier) {
try {
String txId = UUID.randomUUID().toString();
// start a tx and write the first entry
db.txStarted(txId);
db.put(ourEntry.id(), ourEntry.content());
// now both transactions should've written something without committing so test changes are not visible
Assert.assertTrue(db.containsKey(ourEntry.id()));
Assert.assertFalse(db.containsKey(otherEntry.id()));
// make some changes to ourEntry
BasicDocument updatedDoc = new BasicDocument();
db.put(ourEntry.id(), updatedDoc);
// check that the changes are only visible to ourselves...
Document actualDocument = db.getEntry(ourEntry.id()).content();
Assert.assertTrue(db.containsKey(ourEntry.id()));
Assert.assertFalse(db.containsKey(otherEntry.id()));
Assert.assertEquals(updatedDoc, actualDocument);
syncBarrier.await();
// and wait for the other tx to make its own changes....
// now commit
db.txCommitted(txId);
syncBarrier.await();
// check that outside changes are visible...
Assert.assertTrue(db.containsKey(otherEntry.id()));
Document otherDocument = db.getEntry(otherEntry.id()).content();
Assert.assertEquals(updatedDoc, otherDocument);
// start a new tx
txId = UUID.randomUUID().toString();
db.txStarted(txId);
// remove entry entry
db.remove(ourEntry.id());
// and wait for the other tx to remove
syncBarrier.await();
// check that changes are not yet visible...
Assert.assertFalse(db.containsKey(ourEntry.id()));
Assert.assertTrue(db.containsKey(otherEntry.id()));
// and wait for the other tx to remove
syncBarrier.await();
// commit the new tx
db.txCommitted(txId);
// and wait for the other tx to remove
syncBarrier.await();
// check that changes are not now visible...
Assert.assertFalse(db.containsKey(ourEntry.id()));
Assert.assertFalse(db.containsKey(otherEntry.id()));
} catch (RuntimeException re) {
syncBarrier.reset();
throw re;
} catch (Throwable t) {
t.printStackTrace();
syncBarrier.reset();
throw new RuntimeException(t);
}
}
protected <T> T simulateTransaction(Callable<T> operation) throws Exception {
db.txStarted("0");
T result = operation.call();
db.txCommitted("0");
return result;
}
protected SchematicEntry writeSingleEntry() throws Exception {
return simulateTransaction(() -> {
SchematicEntry entry = SchematicEntry.create(UUID.randomUUID().toString(), DEFAULT_CONTENT);
db.putEntry(entry.source());
return entry;
});
}
protected List<SchematicEntry> randomEntries(int sampleSize) throws Exception {
return IntStream.range(0, sampleSize).mapToObj(i -> SchematicEntry.create(
UUID.randomUUID().toString(), DEFAULT_CONTENT)).collect(Collectors.toList());
}
protected void print(String s) {
if (print) {
System.out.println(Thread.currentThread().getName() + ": " + s);
}
}
}