/*
* Copyright (c) 2017 OBiBa. All rights reserved.
*
* This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.obiba.magma.support;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import org.obiba.magma.Datasource;
import org.obiba.magma.DatasourceCopierProgressListener;
import org.obiba.magma.Value;
import org.obiba.magma.ValueSet;
import org.obiba.magma.ValueTable;
import org.obiba.magma.ValueTableWriter;
import org.obiba.magma.ValueTableWriter.ValueSetWriter;
import org.obiba.magma.Variable;
import org.obiba.magma.VariableEntity;
import org.obiba.magma.VariableValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
public class MultithreadedDatasourceCopier {
private static final Logger log = LoggerFactory.getLogger(MultithreadedDatasourceCopier.class);
private static final int BUFFER_SIZE = 150;
@SuppressWarnings({ "UnusedDeclaration", "ParameterHidesMemberVariable" })
public static class Builder {
MultithreadedDatasourceCopier copier = new MultithreadedDatasourceCopier();
public Builder() {
}
public static Builder newCopier() {
return new Builder();
}
public Builder withThreads(ThreadFactory factory) {
copier.threadFactory = factory;
return this;
}
public Builder withQueueSize(int size) {
copier.bufferSize = size;
return this;
}
public Builder withReaders(int readers) {
copier.concurrentReaders = readers;
return this;
}
public Builder withReaderListener(ReaderListener readerListener) {
copier.readerListener = readerListener;
return this;
}
public Builder withProgressListener(@Nullable DatasourceCopierProgressListener progressListener) {
if(progressListener != null) copier.progressListeners.add(progressListener);
return this;
}
public Builder withCopier(@NotNull DatasourceCopier.Builder copier) {
this.copier.copier = copier;
return this;
}
public Builder from(@NotNull ValueTable source) {
copier.sourceTable = source;
if(copier.destinationName == null) {
copier.destinationName = source.getName();
}
return this;
}
public Builder to(Datasource destination) {
copier.destinationDatasource = destination;
return this;
}
public Builder as(String name) {
copier.destinationName = name;
return this;
}
public MultithreadedDatasourceCopier build() {
return copier;
}
}
public interface ReaderListener {
void onRead(ValueSet valueSet, Value... values);
}
@Nullable
private ThreadFactory threadFactory;
private int bufferSize = BUFFER_SIZE;
private int concurrentReaders = 3;
@NotNull
private DatasourceCopier.Builder copier = DatasourceCopier.Builder.newCopier();
private ValueTable sourceTable;
private String destinationName;
private Datasource destinationDatasource;
@NotNull
private VariableValueSource sources[];
private Variable variables[];
private final List<Future<?>> readers = Lists.newArrayList();
private long entitiesToCopy = 0;
@SuppressWarnings("FieldMayBeFinal")
private long entitiesCopied = 0;
private int nextPercentIncrement = 0;
private ReaderListener readerListener;
private final List<DatasourceCopierProgressListener> progressListeners = Lists.newArrayList();
@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR",
justification = "Fields will be populated by Builder")
private MultithreadedDatasourceCopier() {
}
public void copy() throws IOException {
ThreadPoolExecutor executor = (ThreadPoolExecutor) (threadFactory == null //
? Executors.newFixedThreadPool(concurrentReaders) //
: Executors.newFixedThreadPool(concurrentReaders, threadFactory));
prepareVariables();
// A queue containing all entity values available for writing to the destinationDatasource.
BlockingQueue<VariableEntityValues> writeQueue = new LinkedBlockingDeque<>(bufferSize);
DatasourceCopier datasourceCopier = copier.build();
if(datasourceCopier.isCopyValues()) {
// A queue containing all entities to read the values for.
// Once this is empty, and all readers are done, then reading is over.
BlockingQueue<VariableEntity> readQueue = new LinkedBlockingDeque<>(sourceTable.getVariableEntities());
entitiesToCopy = readQueue.size();
for(int i = 0; i < concurrentReaders; i++) {
readers.add(
executor.submit(new ConcurrentValueSetReader(readQueue, writeQueue, datasourceCopier.isCopyNullValues())));
}
}
try {
write(writeQueue);
checkReadersForException();
} finally {
log.debug("Finished multi-threaded copy. Submitted tasks {}, executed tasks {}", executor.getTaskCount(),
executor.getCompletedTaskCount());
executor.shutdownNow();
}
}
private void write(BlockingQueue<VariableEntityValues> writeQueue) throws IOException {
copyVariables();
// The writers could also be concurrent, but dues to transaction isolation issues, it is currently ran
// synchronously
new ConcurrentValueSetWriter(writeQueue).run();
}
@SuppressWarnings("OverlyNestedMethod")
private void checkReadersForException() {
for(Future<?> reader : readers) {
try {
reader.get();
} catch(InterruptedException e) {
throw new RuntimeException(e);
} catch(ExecutionException e) {
Throwable cause = e.getCause();
if(cause != null) {
if(cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
}
throw new RuntimeException(e);
}
}
}
private void prepareVariables() {
List<VariableValueSource> list = Lists.newArrayList();
List<Variable> vars = Lists.newArrayList();
for(Variable variable : sourceTable.getVariables()) {
list.add(sourceTable.getVariableValueSource(variable.getName()));
vars.add(variable);
}
sources = list.toArray(new VariableValueSource[list.size()]);
variables = vars.toArray(new Variable[list.size()]);
}
private void copyVariables() throws IOException {
DatasourceCopier variableCopier = copier.build();
if(variableCopier.isCopyMetadata()) {
variableCopier.setCopyValues(false);
variableCopier.copy(sourceTable, destinationName, destinationDatasource);
}
}
private static class VariableEntityValues {
private final ValueSet valueSet;
private final Value[] values;
private VariableEntityValues(ValueSet valueSet, Value... values) {
this.valueSet = valueSet;
this.values = values;
}
}
private class ConcurrentValueSetReader implements Runnable {
private final BlockingQueue<VariableEntity> readQueue;
private final BlockingQueue<VariableEntityValues> writeQueue;
private final boolean copyNullValues;
private ConcurrentValueSetReader(BlockingQueue<VariableEntity> readQueue,
BlockingQueue<VariableEntityValues> writeQueue, boolean copyNullValues) {
this.readQueue = readQueue;
this.writeQueue = writeQueue;
this.copyNullValues = copyNullValues;
}
@Override
public void run() {
try {
List<VariableEntity> entities = Lists.newArrayList();
VariableEntity entity;
while(entities.size() < sourceTable.getVariableEntityBatchSize() && (entity = readQueue.poll()) != null) {
if (sourceTable.hasValueSet(entity)) {
entities.add(entity);
}
if (entities.size() == sourceTable.getVariableEntityBatchSize()) {
for (ValueSet valueSet : sourceTable.getValueSets(entities)) {
copyValueSet(valueSet);
}
entities.clear();
}
}
if (entities.size() > 0) {
for (ValueSet valueSet : sourceTable.getValueSets(entities)) {
copyValueSet(valueSet);
}
}
} catch(InterruptedException ignored) {
}
}
private void copyEntity(VariableEntity entity) throws InterruptedException {
if (!sourceTable.hasValueSet(entity)) return;
copyValueSet(sourceTable.getValueSet(entity));
}
private void copyValueSet(ValueSet valueSet) throws InterruptedException {
boolean hasOnlyNullValues = true;
Value[] values = new Value[sources.length];
for(int i = 0; i < sources.length; i++) {
Value value = sources[i].getValue(valueSet);
values[i] = value;
hasOnlyNullValues &= value.isNull();
}
if(copyNullValues || !hasOnlyNullValues) {
log.trace("Enqueued entity {}", valueSet.getVariableEntity().getIdentifier());
writeQueue.put(new VariableEntityValues(valueSet, values));
} else {
log.trace("Skip entity {} because of null values", valueSet.getVariableEntity().getIdentifier());
}
if(readerListener != null) {
readerListener.onRead(valueSet, values);
}
}
}
private class ConcurrentValueSetWriter implements Runnable {
private final BlockingQueue<VariableEntityValues> writeQueue;
private ConcurrentValueSetWriter(BlockingQueue<VariableEntityValues> writeQueue) {
this.writeQueue = writeQueue;
}
/**
* Reads the next instance to write. This is a blocking operation. If nothing is left to write, this method will
* return null.
*
* @return
*/
VariableEntityValues next() {
try {
VariableEntityValues values = writeQueue.poll(1, TimeUnit.SECONDS);
// If values is null, then it's either because we haven't done reading or we've finished reading
while(values == null && !isReadCompleted()) {
values = writeQueue.poll(1, TimeUnit.SECONDS);
}
return values;
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* Returns true when all readers have finished submitting to the writeQueue. false otherwise.
*
* @return
*/
private boolean isReadCompleted() {
for(Future<?> reader : readers) {
if(!reader.isDone()) {
return false;
}
}
return true;
}
@SuppressWarnings("ThrowFromFinallyBlock")
@Override
public void run() {
DatasourceCopier datasourceCopier = copier.build();
try(ValueTableWriter tableWriter = datasourceCopier
.innerValueTableWriter(sourceTable, destinationName, destinationDatasource)) {
VariableEntityValues values = null;
while((values = next()) != null) {
copyValue(datasourceCopier, tableWriter, values);
}
}
}
@SuppressWarnings("ThrowFromFinallyBlock")
private void copyValue(DatasourceCopier datasourceCopier, ValueTableWriter tableWriter,
VariableEntityValues values) {
try(ValueSetWriter writer = tableWriter.writeValueSet(values.valueSet.getVariableEntity())) {
// Copy the ValueSet to the destinationDatasource
log.trace("Dequeued entity {}", values.valueSet.getVariableEntity().getIdentifier());
datasourceCopier.copyValues(sourceTable, destinationName, values.valueSet, variables, values.values, writer);
}
entitiesCopied++;
printProgress();
}
@SuppressWarnings("NumericCastThatLosesPrecision")
private void printProgress() {
try {
if(entitiesToCopy > 0) {
int percentComplete = (int) (entitiesCopied / (double) entitiesToCopy * 100);
if(percentComplete >= nextPercentIncrement) {
log.info("Copy {}% complete.", percentComplete);
for(DatasourceCopierProgressListener listener : progressListeners) {
listener.status(sourceTable.getName(), entitiesCopied, entitiesToCopy, percentComplete);
}
nextPercentIncrement = percentComplete + 1;
}
}
} catch(RuntimeException e) {
// Ignore
}
}
}
}