/* * 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.concurrent; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadFactory; import org.obiba.magma.Value; import org.obiba.magma.ValueSet; import org.obiba.magma.ValueTable; 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.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @SuppressWarnings("UnusedDeclaration") public class ConcurrentValueTableReader { private static final int BUFFER_SIZE = 200; private static final Logger log = LoggerFactory.getLogger(ConcurrentValueTableReader.class); private boolean ignoreReadErrors = false; private ThreadFactory threadFactory; private ConcurrentReaderCallback callback; private int nbConcurrentReaders = Runtime.getRuntime().availableProcessors() * 2; private ValueTable valueTable; private Iterable<Variable> variablesFilter; private Iterable<VariableEntity> entitiesFilter; private Variable[] variables; private BlockingQueue<VariableEntityValues> writeQueue; private int bufferSize = BUFFER_SIZE; private ConcurrentValueTableReader() { } public void read() { ExecutorService executorService = threadFactory == null ? Executors.newFixedThreadPool(nbConcurrentReaders) : Executors.newFixedThreadPool(nbConcurrentReaders, threadFactory); variables = Iterables .toArray(variablesFilter == null ? valueTable.getVariables() : variablesFilter, Variable.class); VariableValueSource[] variableValueSources = getVariableValueSources(); List<VariableEntity> entities = ImmutableList .copyOf(entitiesFilter == null ? valueTable.getVariableEntities() : entitiesFilter); // 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<>(entities); writeQueue = new LinkedBlockingDeque<>(bufferSize); try { callback.onBegin(entities, variables); List<Future<?>> readers = entities.isEmpty() ? new ArrayList<Future<?>>() : concurrentRead(executorService, variableValueSources, readQueue); callback.onComplete(); waitForReaders(readers); } finally { executorService.shutdownNow(); } } private List<Future<?>> concurrentRead(ExecutorService executorService, VariableValueSource[] variableValueSources, BlockingQueue<VariableEntity> readQueue) { List<Future<?>> readers = Lists.newArrayList(); for(int i = 0; i < nbConcurrentReaders; i++) { readers.add(executorService.submit(new ConcurrentValueSetReader(variableValueSources, readQueue, writeQueue))); } while(!isReadCompleted(readers)) { flushQueue(); } // Flush remaining values if any // This is necessary due to a race condition between isReadComplete() and readers appending to the write queue flushQueue(); return readers; } private VariableValueSource[] getVariableValueSources() { VariableValueSource[] variableValueSources = new VariableValueSource[variables.length]; for(int i = 0; i < variables.length; i++) { variableValueSources[i] = valueTable.getVariableValueSource(variables[i].getName()); } return variableValueSources; } private void flushQueue() { VariableEntityValues values; while((values = writeQueue.poll()) != null) { callback.onValues(values.getEntity(), variables, values.getValues()); log.trace("write onCallback for entity {}", values.getEntity().getIdentifier()); } } /** * Returns true when all readers have finished submitting to the writeQueue. false otherwise. * * @return */ private boolean isReadCompleted(Iterable<Future<?>> readers) { for(Future<?> reader : readers) { if(!reader.isDone()) { return false; } } return true; } private void waitForReaders(Iterable<Future<?>> readers) { 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(cause); } } } private VariableValueSource[] prepareSources( @SuppressWarnings("ParameterHidesMemberVariable") Variable... variables) { VariableValueSource[] sources = new VariableValueSource[variables.length]; for(int i = 0; i < variables.length; i++) { sources[i] = valueTable.getVariableValueSource(variables[i].getName()); } return sources; } private static class VariableEntityValues { private final VariableEntity entity; private final Value[] values; private VariableEntityValues(VariableEntity entity, Value... values) { this.entity = entity; this.values = values; } public VariableEntity getEntity() { return entity; } @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "EI_EXPOSE_REP") public Value[] getValues() { return values; } } private class ConcurrentValueSetReader implements Runnable { private final VariableValueSource[] sources; private final BlockingQueue<VariableEntity> readQueue; private final BlockingQueue<VariableEntityValues> writeQueue; private ConcurrentValueSetReader(VariableValueSource[] sources, BlockingQueue<VariableEntity> readQueue, BlockingQueue<VariableEntityValues> writeQueue) { this.sources = sources; this.readQueue = readQueue; this.writeQueue = writeQueue; } @Override public void run() { try { VariableEntity entity = readQueue.poll(); while(entity != null && !callback.isCancelled()) { if(valueTable.hasValueSet(entity)) { log.trace("Read entity {}", entity.getIdentifier()); writeQueue.put(new VariableEntityValues(entity, readValues(entity))); } entity = readQueue.poll(); } } catch(InterruptedException e) { // do nothing } } private Value[] readValues(VariableEntity entity) { ValueSet valueSet = valueTable.getValueSet(entity); Value[] values = new Value[sources.length]; for(int i = 0; i < sources.length; i++) { try { values[i] = sources[i].getValue(valueSet); } catch(RuntimeException e) { log.debug("Read exception", e); if(ignoreReadErrors) { values[i] = sources[i].getValueType().nullValue(); } else { throw e; } } } return values; } } @SuppressWarnings("ParameterHidesMemberVariable") public interface ConcurrentReaderCallback { /** * Called before reading starts. The method is provided with the list of entities and variables that will be read * concurrently. * * @param entities entities that will be read * @param variables variables that will be read */ void onBegin(List<VariableEntity> entities, Variable... variables); /** * Called when a set of {@code Value} has been read and is ready to be written. This method is not called * concurrently. Implementations are not required to be threadsafe. * * @param entity the {@code VariableEntity} that has been read * @param variables the {@code Variable} instances for which the values were read * @param values the {@code Value} instances, one per variable */ void onValues(VariableEntity entity, Variable[] variables, Value... values); /** * Called when all entities have been read. */ void onComplete(); /** * Request for readers to cancel prematurely. * * @return */ boolean isCancelled(); } @SuppressWarnings("ParameterHidesMemberVariable") public static class Builder { ConcurrentValueTableReader reader = new ConcurrentValueTableReader(); public Builder() { } public static Builder newReader() { return new Builder(); } public Builder withThreads(ThreadFactory factory) { reader.threadFactory = factory; return this; } public Builder withReaders(int readers) { reader.nbConcurrentReaders = readers; return this; } public Builder withBufferSize(int bufferSize) { reader.bufferSize = bufferSize; return this; } public Builder from(ValueTable source) { reader.valueTable = source; return this; } public Builder to(ConcurrentReaderCallback callback) { reader.callback = callback; return this; } public Builder variablesFilter(Iterable<Variable> variablesFilter) { reader.variablesFilter = variablesFilter; return this; } public Builder entitiesFilter(Iterable<VariableEntity> entitiesFilter) { reader.entitiesFilter = entitiesFilter; return this; } public Builder ignoreReadErrors() { reader.ignoreReadErrors = true; return this; } public ConcurrentValueTableReader build() { return reader; } } }