/*
* Copyright 2013 Gordon Burgett and individual contributors
*
* 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.xflatdb.xflat.db;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import junit.framework.AssertionFailedError;
import org.hamcrest.Matchers;
import org.jdom2.Element;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;
import org.junit.BeforeClass;
import org.junit.Test;
import org.xflatdb.xflat.Table;
import test.Foo;
import test.Utils;
import static org.junit.Assert.*;
import org.xflatdb.xflat.query.XPathQuery;
import org.xflatdb.xflat.query.XPathUpdate;
import org.xflatdb.xflat.transaction.Propagation;
import org.xflatdb.xflat.transaction.TransactionOptions;
import org.xflatdb.xflat.transaction.TransactionScope;
/**
*
* @author Gordon
*/
public class MultithreadedDbIntegrationTests {
static File workspace = new File("integrationtests");
@BeforeClass
public static void setUpClass(){
if(workspace.exists()){
Utils.deleteDir(workspace);
}
}
private XFlatDatabase getDatabase(String testName){
File dbDir = new File(workspace, testName);
XFlatDatabase ret = new XFlatDatabase(dbDir);
return ret;
}
private List<Thread> run(Runnable r, int threads){
List<Thread> ret = new ArrayList<>();
for(int i = 0; i < threads; i++){
ret.add(new Thread(r));
}
for(int i = 0; i < threads; i++){
ret.get(i).start();
}
return ret;
}
private void spinWait(long nanos){
long start = System.nanoTime();
long diff = 0;
do{
diff = System.nanoTime() - start;
if((diff & 0xFFFF) == 0L) //approx. 65 uS
Thread.yield();
}while(diff - nanos < 0);
}
private static final Random seedRandom = new Random();
private ThreadLocal<Random> random = new ThreadLocal<Random>(){
@Override
protected Random initialValue(){
return new Random(seedRandom.nextLong());
}
};
@Test
public void HeavyRead_OneUpdate_AfterInsertAllGetNewValue() throws Exception {
System.out.println("HeavyRead_OneUpdate_AfterInsertAllGetNewValue");
final AtomicBoolean finished = new AtomicBoolean(false);
final XFlatDatabase db = getDatabase("HeavyRead_OneUpdate_AfterInsertAllGetNewValue");
db.getConversionService().addConverter(Foo.class, Element.class, new Foo.ToElementConverter());
db.getConversionService().addConverter(Element.class, Foo.class, new Foo.FromElementConverter());
db.initialize();
try{
final Foo oldFoo = new Foo();
oldFoo.fooInt = 5;
oldFoo.setId("1");
final Foo newFoo = new Foo();
newFoo.fooInt = 6;
newFoo.setId("1");
final AtomicInteger counter = new AtomicInteger(0);
final List<Integer> allOldFooCounters = new ArrayList<>();
final List<Integer> allNewFooCounters = new ArrayList<>();
//set up readers
Runnable r = new Runnable(){
@Override
public void run() {
List<Integer> oldFooCounters = new ArrayList<>();
List<Integer> newFooCounters = new ArrayList<>();
Table<Foo> fooTable = db.getTable(Foo.class);
while(!finished.get()){
Integer count = counter.incrementAndGet();
Foo foo = fooTable.find("1");
if(oldFoo.equals(foo))
oldFooCounters.add(count);
else if(newFoo.equals(foo))
newFooCounters.add(count);
}
synchronized(allOldFooCounters){
allOldFooCounters.addAll(oldFooCounters);
}
synchronized(allNewFooCounters){
allNewFooCounters.addAll(newFooCounters);
}
}
};
Table<Foo> fooTable = db.getTable(Foo.class);
fooTable.insert(oldFoo);
Integer before;
Integer after;
List<Thread> th = run(r, 3);
Thread.sleep(10);
//act
before = counter.incrementAndGet();
fooTable.replace(newFoo);
after = counter.incrementAndGet();
finished.set(true);
for(Thread t : th){
t.join();
}
//ASSERT
//there may be instances between "before" and "after" where we find both,
//but all the instances of "OldFoo" should definitely be before "after"
//and all instances of "NewFoo" should definitely be after "before".
assertThat("All counters in oldFooCounters should be prior to 'after'",
allOldFooCounters, Matchers.everyItem(Matchers.lessThan(after)));
//assert all new foo counters are post-after
assertThat("All counters in newFooCounters should be post 'before'",
allNewFooCounters, Matchers.everyItem(Matchers.greaterThan(before)));
}finally{
finished.set(true);
db.shutdown();
}
}
@Test
public void HeavyWrite_OneReader_AllReadsInOrder() throws Exception {
System.out.println("HeavyWrite_OneReader_AllReadsInOrder");
final AtomicBoolean finished = new AtomicBoolean(false);
final XFlatDatabase db = getDatabase("HeavyWrite_OneReader_AllReadsInOrder");
db.getConversionService().addConverter(Foo.class, Element.class, new Foo.ToElementConverter());
db.getConversionService().addConverter(Element.class, Foo.class, new Foo.FromElementConverter());
db.initialize();
try{
final AtomicInteger counter = new AtomicInteger(0);
final AtomicInteger idStarter = new AtomicInteger(0);
final AtomicInteger numInserts = new AtomicInteger(0);
final AtomicReference<Exception> lastException = new AtomicReference<>(null);
Runnable r = new Runnable(){
@Override
public void run() {
try{
String id = Integer.toString(idStarter.incrementAndGet());
Table<Foo> fooTable = db.getTable(Foo.class);
while(!finished.get()){
Foo foo = new Foo();
foo.fooInt = counter.incrementAndGet();
//put it in one of 3 ID slots
foo.setId(id);
boolean inserted = fooTable.upsert(foo);
if(inserted)
numInserts.incrementAndGet();
}
//do one more - this will be the final one
Foo foo = new Foo();
foo.fooInt = counter.incrementAndGet();
//put it in one of 4 ID slots
foo.setId(id);
boolean inserted = fooTable.upsert(foo);
if(inserted)
numInserts.incrementAndGet();
}catch(Exception ex){
System.err.println(ex.toString());
lastException.set(ex);
}
}
};
Table<Foo> fooTable = db.getTable(Foo.class);
List<Integer> foos = new ArrayList<>(4);
Integer after, before;
List<Thread> th = run(r, 3);
Thread.sleep(10);
before = counter.incrementAndGet();
finished.set(true);
for(Thread t : th){
t.join();
}
//ASSERT
if(lastException.get() != null){
fail(lastException.get().getMessage());
}
foos.add(fooTable.find("1").fooInt);
foos.add(fooTable.find("2").fooInt);
foos.add(fooTable.find("3").fooInt);
after = counter.incrementAndGet();
assertThat(foos, Matchers.everyItem(Matchers.lessThan(after)));
assertThat(foos, Matchers.everyItem(Matchers.greaterThan(before)));
assertEquals(3, numInserts.get());
}
finally{
finished.set(true);
db.shutdown();
}
}
@Test
public void testTransactionalWrites_InOwnThread_MaintainsIsolation() throws Exception {
System.out.println("testTransactionalWrites_InOwnThread_MaintainsIsolation");
final AtomicBoolean finished = new AtomicBoolean(false);
final XFlatDatabase db = getDatabase("HeavyWrite_OneReader_AllReadsInOrder");
db.getConversionService().addConverter(Foo.class, Element.class, new Foo.ToElementConverter());
db.getConversionService().addConverter(Element.class, Foo.class, new Foo.FromElementConverter());
db.initialize();
try{
final AtomicInteger counter = new AtomicInteger(0);
final AtomicInteger idStarter = new AtomicInteger(0);
final AtomicReference<Throwable> lastException = new AtomicReference<>(null);
final XPathExpression<Object> fooInt = XPathFactory.instance().compile("foo/fooInt");
Runnable r = new Runnable() {
@Override
public void run() {
int start = idStarter.incrementAndGet();
try{
Table<Foo> fooTable = db.getTable(Foo.class);
Map<String, Foo> insertedFoos = new HashMap<>();
//should be isolated
try(TransactionScope tx = db.getTransactionManager().openTransaction(TransactionOptions.DEFAULT.withPropagation(Propagation.REQUIRES_NEW))){
int count = start;
while((++count % 100) != start){
//go around the loop till we get back to start
Foo foo = new Foo();
foo.fooInt = random.get().nextInt();
//drop it in the "count" ID. We are definitely
//stepping on the toes of another thread here,
//but hopefully not at the same time to avoid synclock.
foo.setId(Integer.toString(count));
//ought not to throw DuplicateKeyException
fooTable.insert(foo);
insertedFoos.put(foo.getId(), foo);
}
for(Map.Entry<String, Foo> foos : insertedFoos.entrySet()){
//we ought to be able to find the data in the DB
Foo foo = foos.getValue();
Foo inDb = fooTable.find(foos.getKey());
assertEquals("Unequal foos at ID " + foos.getKey(), foo, inDb);
}
int rand = random.get().nextInt(insertedFoos.size());
int cnt = 0;
Foo foo = null;
for(Foo f : insertedFoos.values()){
if(cnt++ == rand){
foo = f;
break;
}
}
int newFooInt = random.get().nextInt();
fooTable.update(XPathQuery.eq(fooInt, foo.fooInt), XPathUpdate.set(fooInt, newFooInt));
foo = fooTable.find(foo.getId());
assertEquals("Should update in TX", newFooInt, foo.fooInt);
}
}catch(Throwable ex){
synchronized(this){
System.err.println("Error in thread with start " + start);
System.err.println(ex.toString());
System.err.println(ex.getStackTrace());
}
lastException.set(ex);
}
}
};
//ACT
Table<Foo> fooTable = db.getTable(Foo.class);
List<Foo> inMainThread = new ArrayList<>();
List<Thread> threads = run(r, 3);
//should see nothing in DB while these transactions are running
inMainThread.add(fooTable.find("1"));
inMainThread.add(fooTable.find("2"));
inMainThread.add(fooTable.find("3"));
inMainThread.add(fooTable.find("4"));
Foo foo = new Foo();
foo.fooInt = 7;
fooTable.insert(foo);
Foo inDb = fooTable.find(foo.getId());
for(Thread t : threads)
t.join();
//ASSERT
if(lastException.get() != null){
throw new Exception("Failure while processing", lastException.get());
}
assertThat(inMainThread, Matchers.everyItem(Matchers.nullValue(Foo.class)));
assertEquals("Should have retrieved non-transactional data", foo, inDb);
assertEquals("Should have retrieved non-transactional data", foo, fooTable.find(foo.getId()));
}finally{
finished.set(true);
db.shutdown();
}
}
}