/*******************************************************************************
* This file is part of RedReader.
*
* RedReader is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* RedReader 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with RedReader. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package org.quantumbadger.redreader.io;
import org.quantumbadger.redreader.common.TimestampBound;
import org.quantumbadger.redreader.common.TriggerableThread;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadedRawObjectDB<K, V extends WritableObject<K>, F>
implements CacheDataSource<K, V, F> {
private final TriggerableThread writeThread = new TriggerableThread(new Runnable() {
@Override
public void run() {
doWrite();
}
}, 1500);
private final TriggerableThread readThread = new TriggerableThread(new Runnable() {
@Override
public void run() {
doRead();
}
}, 0);
private final HashMap<K, V> toWrite = new HashMap<>();
private final LinkedBlockingQueue<ReadOperation> toRead = new LinkedBlockingQueue<>();
private final Object ioLock = new Object();
private final RawObjectDB<K, V> db;
private final CacheDataSource<K, V, F> alternateSource;
public ThreadedRawObjectDB(RawObjectDB<K, V> db, CacheDataSource<K, V, F> alternateSource) {
this.db = db;
this.alternateSource = alternateSource;
}
private void doWrite() {
synchronized(ioLock) {
final ArrayList<V> values;
synchronized(toWrite) {
values = new ArrayList<>(toWrite.values());
toWrite.clear();
}
db.putAll(values);
}
}
private void doRead() {
synchronized(ioLock) {
while(!toRead.isEmpty()) {
toRead.remove().run();
}
}
}
public void performRequest(K key, TimestampBound timestampBound,
RequestResponseHandler<V, F> handler) {
toRead.offer(new SingleReadOperation(timestampBound, handler, key));
readThread.trigger();
}
public void performRequest(Collection<K> keys, TimestampBound timestampBound,
RequestResponseHandler<HashMap<K, V>, F> handler) {
toRead.offer(new BulkReadOperation(timestampBound, handler, keys));
readThread.trigger();
}
public void performWrite(V value) {
synchronized(toWrite) {
toWrite.put(value.getKey(), value);
}
writeThread.trigger();
}
public void performWrite(Collection<V> values) {
synchronized(toWrite) {
for(V value : values) {
toWrite.put(value.getKey(), value);
}
}
writeThread.trigger();
}
private class BulkReadOperation extends ReadOperation {
public final Collection<K> keys;
public final RequestResponseHandler<HashMap<K, V>, F> responseHandler;
private BulkReadOperation(TimestampBound timestampBound,
RequestResponseHandler<HashMap<K, V>, F> responseHandler, Collection<K> keys) {
super(timestampBound);
this.responseHandler = responseHandler;
this.keys = keys;
}
@Override
public void run() {
final HashMap<K, V> existingResult = new HashMap<>(keys.size());
long oldestTimestamp = Long.MAX_VALUE;
synchronized(toWrite) {
final Iterator<K> iter = keys.iterator();
while(iter.hasNext()) {
final K key = iter.next();
final V writeCacheResult = toWrite.get(key);
if(writeCacheResult != null && timestampBound.verifyTimestamp(writeCacheResult.getTimestamp())) {
iter.remove();
existingResult.put(key, writeCacheResult);
oldestTimestamp = Math.min(oldestTimestamp, writeCacheResult.getTimestamp());
}
}
}
if(keys.size() == 0) {
responseHandler.onRequestSuccess(existingResult, oldestTimestamp);
return;
}
final Iterator<K> iter = keys.iterator();
while(iter.hasNext()) {
final K key = iter.next();
final V dbResult = db.getById(key); // TODO this is pretty inefficient
if(dbResult != null && timestampBound.verifyTimestamp(dbResult.getTimestamp())) {
iter.remove();
existingResult.put(key, dbResult);
oldestTimestamp = Math.min(oldestTimestamp, dbResult.getTimestamp());
}
}
if(keys.size() == 0) {
responseHandler.onRequestSuccess(existingResult, oldestTimestamp);
return;
}
final long outerOldestTimestamp = oldestTimestamp;
alternateSource.performRequest(keys, timestampBound, new RequestResponseHandler<HashMap<K, V>, F>() {
public void onRequestFailed(F failureReason) {
responseHandler.onRequestFailed(failureReason);
}
public void onRequestSuccess(HashMap<K, V> result, long timeCached) {
performWrite(result.values());
existingResult.putAll(result);
responseHandler.onRequestSuccess(existingResult, Math.min(timeCached, outerOldestTimestamp));
}
});
}
}
private class SingleReadOperation extends ReadOperation {
public final K key;
public final RequestResponseHandler<V, F> responseHandler;
private SingleReadOperation(TimestampBound timestampBound, RequestResponseHandler<V, F> responseHandler, K key) {
super(timestampBound);
this.responseHandler = responseHandler;
this.key = key;
}
@Override
public void run() {
synchronized(toWrite) {
final V writeCacheResult = toWrite.get(key);
if(writeCacheResult != null && timestampBound.verifyTimestamp(writeCacheResult.getTimestamp())) {
responseHandler.onRequestSuccess(writeCacheResult, writeCacheResult.getTimestamp());
return;
}
}
final V dbResult = db.getById(key);
if(dbResult != null && timestampBound.verifyTimestamp(dbResult.getTimestamp())) {
responseHandler.onRequestSuccess(dbResult, dbResult.getTimestamp());
return;
}
alternateSource.performRequest(key, timestampBound, new RequestResponseHandler<V, F>() {
public void onRequestFailed(F failureReason) {
responseHandler.onRequestFailed(failureReason);
}
public void onRequestSuccess(V result, long timeCached) {
performWrite(result);
responseHandler.onRequestSuccess(result, timeCached);
}
});
}
}
private abstract class ReadOperation {
public final TimestampBound timestampBound;
private ReadOperation(TimestampBound timestampBound) {
this.timestampBound = timestampBound;
}
public abstract void run();
}
}