/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ethereumJ library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.datasource;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.ethereum.datasource.inmem.HashMapDB;
import org.ethereum.datasource.leveldb.LevelDbDataSource;
import org.ethereum.db.StateSource;
import org.ethereum.mine.AnyFuture;
import org.ethereum.util.ALock;
import org.ethereum.util.ByteArrayMap;
import org.ethereum.util.Utils;
import org.ethereum.vm.DataWord;
import org.junit.Assert;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static java.lang.Math.max;
import static java.lang.Thread.sleep;
import static org.ethereum.crypto.HashUtil.sha3;
import static org.ethereum.util.ByteUtil.intToBytes;
import static org.ethereum.util.ByteUtil.longToBytes;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
/**
* Testing different sources and chain of sources in multi-thread environment
*/
public class MultiThreadSourcesTest {
private byte[] intToKey(int i) {
return sha3(longToBytes(i));
}
private byte[] intToValue(int i) {
return (new DataWord(i)).getData();
}
private String str(Object obj) {
if (obj == null) return null;
return Hex.toHexString((byte[]) obj);
}
private class TestExecutor {
private Source<byte[], byte[]> cache;
boolean isCounting = false;
boolean noDelete = false;
boolean running = true;
final CountDownLatch failSema = new CountDownLatch(1);
final AtomicInteger putCnt = new AtomicInteger(1);
final AtomicInteger delCnt = new AtomicInteger(1);
final AtomicInteger checkCnt = new AtomicInteger(0);
public TestExecutor(Source cache) {
this.cache = cache;
}
public TestExecutor(Source cache, boolean isCounting) {
this.cache = cache;
this.isCounting = isCounting;
}
public void setNoDelete(boolean noDelete) {
this.noDelete = noDelete;
}
final Thread readThread = new Thread(new Runnable() {
@Override
public void run() {
try {
while(running) {
int curMax = putCnt.get() - 1;
if (checkCnt.get() >= curMax) {
sleep(10);
continue;
}
assertEquals(str(intToValue(curMax)), str(cache.get(intToKey(curMax))));
checkCnt.set(curMax);
}
} catch (Throwable e) {
e.printStackTrace();
failSema.countDown();
}
}
});
final Thread delThread = new Thread(new Runnable() {
@Override
public void run() {
try {
while(running) {
int toDelete = delCnt.get();
int curMax = putCnt.get() - 1;
if (toDelete > checkCnt.get() || toDelete >= curMax) {
sleep(10);
continue;
}
assertEquals(str(intToValue(toDelete)), str(cache.get(intToKey(toDelete))));
if (isCounting) {
for (int i = 0; i < (toDelete % 5); ++i) {
cache.delete(intToKey(toDelete));
assertEquals(str(intToValue(toDelete)), str(cache.get(intToKey(toDelete))));
}
}
cache.delete(intToKey(toDelete));
if (isCounting) cache.flush();
assertNull(cache.get(intToKey(toDelete)));
delCnt.getAndIncrement();
}
} catch (Throwable e) {
e.printStackTrace();
failSema.countDown();
}
}
});
public void run(long timeout) {
new Thread(new Runnable() {
@Override
public void run() {
try {
while(running) {
int curCnt = putCnt.get();
cache.put(intToKey(curCnt), intToValue(curCnt));
if (isCounting) {
for (int i = 0; i < (curCnt % 5); ++i) {
cache.put(intToKey(curCnt), intToValue(curCnt));
}
}
putCnt.getAndIncrement();
if (curCnt == 1) {
readThread.start();
if (!noDelete) {
delThread.start();
}
}
}
} catch (Throwable e) {
e.printStackTrace();
failSema.countDown();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
while(running) {
sleep(10);
cache.flush();
}
} catch (Throwable e) {
e.printStackTrace();
failSema.countDown();
}
}
}).start();
try {
failSema.await(timeout, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
running = false;
throw new RuntimeException("Thrown interrupted exception", ex);
}
// Shutdown carefully
running = false;
if (failSema.getCount() == 0) {
throw new RuntimeException("Test failed.");
} else {
System.out.println("Test passed, put counter: " + putCnt.get() + ", delete counter: " + delCnt.get());
}
}
}
private class TestExecutor1 {
private Source<byte[], byte[]> cache;
public int writerThreads = 4;
public int readerThreads = 8;
public int deleterThreads = 0;
public int flusherThreads = 2;
public int maxKey = 10000;
boolean stopped;
Map<byte[], byte[]> map = Collections.synchronizedMap(new ByteArrayMap<byte[]>());
ReadWriteLock rwLock = new ReentrantReadWriteLock();
ALock rLock = new ALock(rwLock.readLock());
ALock wLock = new ALock(rwLock.writeLock());
public TestExecutor1(Source<byte[], byte[]> cache) {
this.cache = cache;
}
class Writer implements Runnable {
public void run() {
Random rnd = new Random(0);
while (!stopped) {
byte[] key = key(rnd.nextInt(maxKey));
try (ALock l = wLock.lock()) {
map.put(key, key);
cache.put(key, key);
}
Utils.sleep(rnd.nextInt(1));
}
}
}
class Reader implements Runnable {
public void run() {
Random rnd = new Random(0);
while (!stopped) {
byte[] key = key(rnd.nextInt(maxKey));
try (ALock l = rLock.lock()) {
byte[] expected = map.get(key);
byte[] actual = cache.get(key);
Assert.assertArrayEquals(expected, actual);
}
}
}
}
class Deleter implements Runnable {
public void run() {
Random rnd = new Random(0);
while (!stopped) {
byte[] key = key(rnd.nextInt(maxKey));
try (ALock l = wLock.lock()) {
map.remove(key);
cache.delete(key);
}
}
}
}
class Flusher implements Runnable {
public void run() {
Random rnd = new Random(0);
while (!stopped) {
Utils.sleep(rnd.nextInt(50));
cache.flush();
}
}
}
public void start(long time) throws InterruptedException, TimeoutException, ExecutionException {
List<Callable<Object>> all = new ArrayList<>();
for (int i = 0; i < writerThreads; i++) {
all.add(Executors.callable(new Writer()));
}
for (int i = 0; i < readerThreads; i++) {
all.add(Executors.callable(new Reader()));
}
for (int i = 0; i < deleterThreads; i++) {
all.add(Executors.callable(new Deleter()));
}
for (int i = 0; i < flusherThreads; i++) {
all.add(Executors.callable(new Flusher()));
}
ExecutorService executor = Executors.newFixedThreadPool(all.size());
ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(executor);
AnyFuture<Object> anyFuture = new AnyFuture<>();
for (Callable<Object> callable : all) {
ListenableFuture<Object> future = listeningExecutorService.submit(callable);
anyFuture.add(future);
}
try {
anyFuture.get(time, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("Passed.");
}
stopped = true;
}
}
@Test
public void testWriteCache() throws InterruptedException {
Source<byte[], byte[]> src = new HashMapDB<>();
final WriteCache writeCache = new WriteCache.BytesKey<>(src, WriteCache.CacheType.SIMPLE);
TestExecutor testExecutor = new TestExecutor(writeCache);
testExecutor.run(5);
}
@Test
public void testReadCache() throws InterruptedException {
Source<byte[], byte[]> src = new HashMapDB<>();
final ReadCache readCache = new ReadCache.BytesKey<>(src);
TestExecutor testExecutor = new TestExecutor(readCache);
testExecutor.run(5);
}
@Test
public void testReadWriteCache() throws InterruptedException {
Source<byte[], byte[]> src = new HashMapDB<>();
final ReadWriteCache readWriteCache = new ReadWriteCache.BytesKey<>(src, WriteCache.CacheType.SIMPLE);
TestExecutor testExecutor = new TestExecutor(readWriteCache);
testExecutor.run(5);
}
@Test
public void testAsyncWriteCache() throws InterruptedException, TimeoutException, ExecutionException {
Source<byte[], byte[]> src = new HashMapDB<>();
AsyncWriteCache<byte[], byte[]> cache = new AsyncWriteCache<byte[], byte[]>(src) {
@Override
protected WriteCache<byte[], byte[]> createCache(Source<byte[], byte[]> source) {
return new WriteCache.BytesKey<byte[]>(source, WriteCache.CacheType.SIMPLE) {
@Override
public boolean flush() {
// System.out.println("Flushing started");
boolean ret = super.flush();
// System.out.println("Flushing complete");
return ret;
}
};
}
};
// TestExecutor testExecutor = new TestExecutor(cache);
TestExecutor1 testExecutor = new TestExecutor1(cache);
testExecutor.start(5);
}
@Test
public void testStateSource() throws Exception {
HashMapDB<byte[]> src = new HashMapDB<>();
// LevelDbDataSource ldb = new LevelDbDataSource("test");
// ldb.init();
StateSource stateSource = new StateSource(src, false);
stateSource.getReadCache().withMaxCapacity(10);
TestExecutor1 testExecutor = new TestExecutor1(stateSource);
testExecutor.start(10);
}
volatile int maxConcurrency = 0;
volatile int maxWriteConcurrency = 0;
volatile int maxReadWriteConcurrency = 0;
@Test
public void testStateSourceConcurrency() throws Exception {
HashMapDB<byte[]> src = new HashMapDB<byte[]>() {
AtomicInteger concurrentReads = new AtomicInteger(0);
AtomicInteger concurrentWrites = new AtomicInteger(0);
void checkConcurrency(boolean write) {
maxConcurrency = max(concurrentReads.get() + concurrentWrites.get(), maxConcurrency);
if (write) {
maxWriteConcurrency = max(concurrentWrites.get(), maxWriteConcurrency);
} else {
maxReadWriteConcurrency = max(concurrentWrites.get(), maxReadWriteConcurrency);
}
}
@Override
public void put(byte[] key, byte[] val) {
int i1 = concurrentWrites.incrementAndGet();
checkConcurrency(true);
super.put(key, val);
int i2 = concurrentWrites.getAndDecrement();
}
@Override
public byte[] get(byte[] key) {
// Utils.sleep(60_000);
int i1 = concurrentReads.incrementAndGet();
checkConcurrency(false);
Utils.sleep(1);
byte[] ret = super.get(key);
int i2 = concurrentReads.getAndDecrement();
return ret;
}
@Override
public void delete(byte[] key) {
int i1 = concurrentWrites.incrementAndGet();
checkConcurrency(true);
super.delete(key);
int i2 = concurrentWrites.getAndDecrement();
}
};
final StateSource stateSource = new StateSource(src, false);
stateSource.getReadCache().withMaxCapacity(10);
new Thread() {
@Override
public void run() {
stateSource.get(key(1));
}
}.start();
stateSource.get(key(2));
TestExecutor1 testExecutor = new TestExecutor1(stateSource);
// testExecutor.writerThreads = 0;
testExecutor.start(3);
System.out.println("maxConcurrency = " + maxConcurrency + ", maxWriteConcurrency = " + maxWriteConcurrency +
", maxReadWriteConcurrency = " + maxReadWriteConcurrency);
}
@Test
public void testCountingWriteCache() throws InterruptedException {
Source<byte[], byte[]> parentSrc = new HashMapDB<>();
Source<byte[], byte[]> src = new CountingBytesSource(parentSrc);
final WriteCache writeCache = new WriteCache.BytesKey<>(src, WriteCache.CacheType.COUNTING);
TestExecutor testExecutor = new TestExecutor(writeCache, true);
testExecutor.run(10);
}
private static byte[] key(int key) {
return sha3(intToBytes(key));
}
}