package eu.fbk.knowledgestore.datastore;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.openrdf.model.URI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import eu.fbk.knowledgestore.data.Record;
import eu.fbk.knowledgestore.data.Stream;
import eu.fbk.knowledgestore.data.XPath;
import eu.fbk.knowledgestore.internal.Util;
import eu.fbk.knowledgestore.runtime.Component;
import eu.fbk.knowledgestore.runtime.Synchronizer;
/**
* A {@code DataStore} wrapper that synchronizes and enforces a proper access to an another
* {@code DataStore}.
* <p>
* This wrapper provides the following guarantees with respect to external access to the wrapped
* {@link DataStore}:
* <ul>
* <li>transaction are started and committed according to the synchronization strategy enforced by
* a supplied {@link Synchronizer};</li>
* <li>at most one thread at a time can access the wrapped {@code DataStore} and its
* {@code DataTransaction}s, with the only exception of {@link DataStore#close()} and
* {@link DataTransaction#end(boolean)} which may be called concurrently with other active
* operations;</li>
* <li>access to the wrapped {@code DataStore} and its {@code DataTransaction} is enforced to
* occur strictly in adherence with the lifecycle defined for {@link Component}s (
* {@code IllegalStateException}s are thrown to the caller otherwise);</li>
* <li>before a {@code DataTransaction} is ended, all the streams previously returned and still
* open to be forcedly closed;</li>
* <li>before the {@code DataStore} is closed, pending {@code DataTransaction} are forcedly ended
* with a rollback;</li>
* </ul>
* </p>
*/
public final class SynchronizedDataStore extends ForwardingDataStore {
private static final Logger LOGGER = LoggerFactory.getLogger(SynchronizedDataStore.class);
private static final int NEW = 0;
private static final int INITIALIZED = 1;
private static final int CLOSED = 2;
private final DataStore delegate;
private final Synchronizer synchronizer;
private final Set<DataTransaction> transactions; // used also as lock object
private final AtomicInteger state;
/**
* Creates a new instance for the wrapped {@code DataStore} and the {@code Synchronizer}
* specification string supplied.
*
* @param delegate
* the wrapped {@code DataStore}
* @param synchronizerSpec
* the synchronizer specification string (see {@link Synchronizer})
*/
public SynchronizedDataStore(final DataStore delegate, final String synchronizerSpec) {
this(delegate, Synchronizer.create(synchronizerSpec));
}
/**
* Creates a new instance for the wrapped {@code DataStore} and {@code Synchronizer}
* specified.
*
* @param delegate
* the wrapped {@code DataStore}
* @param synchronizer
* the synchronizer responsible to regulate the access to the wrapped
* {@code DataStore}
*/
public SynchronizedDataStore(final DataStore delegate, final Synchronizer synchronizer) {
this.delegate = Preconditions.checkNotNull(delegate);
this.synchronizer = Preconditions.checkNotNull(synchronizer);
this.transactions = Sets.newHashSet();
this.state = new AtomicInteger(NEW);
LOGGER.debug("{} configured, synchronizer=", getClass().getSimpleName(), synchronizer);
}
@Override
protected DataStore delegate() {
return this.delegate;
}
private void checkState(final int expected) {
final int state = this.state.get();
if (state != expected) {
throw new IllegalStateException("DataStore "
+ (state == NEW ? "not initialized"
: state == INITIALIZED ? "already initialized" : "already closed"));
}
}
@Override
public synchronized void init() throws IOException {
checkState(NEW);
super.init();
this.state.set(INITIALIZED);
}
@Override
public DataTransaction begin(final boolean readOnly) throws IOException, IllegalStateException {
checkState(INITIALIZED);
this.synchronizer.beginTransaction(readOnly);
DataTransaction transaction = null;
try {
synchronized (this) {
checkState(INITIALIZED);
transaction = delegate().begin(readOnly);
transaction = new SynchronizedDataTransaction(transaction, readOnly);
synchronized (this.transactions) {
this.transactions.add(transaction);
}
}
} finally {
if (transaction == null) {
this.synchronizer.endTransaction(readOnly);
}
}
return transaction;
}
@Override
public void close() {
if (!this.state.compareAndSet(INITIALIZED, CLOSED)
&& !this.state.compareAndSet(NEW, CLOSED)) {
return;
}
List<DataTransaction> transactionsToEnd;
synchronized (this.transactions) {
transactionsToEnd = Lists.newArrayList(this.transactions);
}
try {
for (final DataTransaction transaction : transactionsToEnd) {
try {
LOGGER.warn("Forcing rollback of DataTransaction " + transaction
+ "due to closure of DataStore");
transaction.end(false);
} catch (final Throwable ex) {
LOGGER.error("Exception caught while ending DataTransaction " + transaction
+ "(rollback assumed): " + ex.getMessage(), ex);
}
}
} finally {
super.close();
}
}
private final class SynchronizedDataTransaction extends ForwardingDataTransaction {
private final DataTransaction delegate;
private final List<WeakReference<Stream<?>>> streams;
private final boolean readOnly;
private final AtomicBoolean ended;
SynchronizedDataTransaction(final DataTransaction delegate, final boolean readOnly) {
this.delegate = Preconditions.checkNotNull(delegate);
this.streams = Lists.newArrayList();
this.readOnly = readOnly;
this.ended = new AtomicBoolean(false);
}
@Override
protected DataTransaction delegate() {
return this.delegate;
}
private <T extends Stream<?>> T registerStream(@Nullable final T stream) {
synchronized (this.streams) {
if (stream == null) {
return null;
} else if (this.ended.get()) {
Util.closeQuietly(stream);
} else {
final int size = this.streams.size();
for (int i = size - 1; i >= 0; --i) {
if (this.streams.get(i).get() == null) {
this.streams.remove(i);
}
}
this.streams.add(new WeakReference<Stream<?>>(stream));
}
}
return stream;
}
private void closeStreams() {
synchronized (this.streams) {
final int size = this.streams.size();
for (int i = size - 1; i >= 0; --i) {
Util.closeQuietly(this.streams.remove(i).get());
}
}
}
private void checkState() {
if (this.ended.get()) {
throw new IllegalStateException("DataTransaction already ended");
}
}
private void checkWritable() {
if (this.readOnly) {
throw new IllegalStateException("DataTransaction is read-only");
}
}
@Override
public synchronized Stream<Record> lookup(final URI type, final Set<? extends URI> ids,
@Nullable final Set<? extends URI> properties) throws IOException,
IllegalArgumentException, IllegalStateException {
checkState();
return registerStream(super.lookup(type, ids, properties));
}
@Override
public synchronized Stream<Record> retrieve(final URI type,
@Nullable final XPath condition, @Nullable final Set<? extends URI> properties)
throws IOException, IllegalArgumentException, IllegalStateException {
checkState();
return registerStream(super.retrieve(type, condition, properties));
}
@Override
public synchronized long count(final URI type, @Nullable final XPath condition)
throws IOException, IllegalArgumentException, IllegalStateException {
checkState();
return super.count(type, condition);
}
@Override
public Stream<Record> match(final Map<URI, XPath> conditions,
final Map<URI, Set<URI>> ids, final Map<URI, Set<URI>> properties)
throws IOException, IllegalStateException {
checkState();
return registerStream(super.match(conditions, ids, properties));
}
@Override
public void store(final URI type, final Record record) throws IOException,
IllegalStateException {
checkState();
checkWritable();
super.store(type, record);
}
@Override
public void delete(final URI type, final URI id) throws IOException, IllegalStateException {
checkState();
checkWritable();
super.delete(type, id);
}
@Override
public void end(final boolean commit) throws IOException, IllegalStateException {
if (!this.ended.compareAndSet(false, true)) {
return;
}
closeStreams();
SynchronizedDataStore.this.synchronizer.beginCommit();
try {
super.end(commit);
} finally {
SynchronizedDataStore.this.synchronizer.endCommit();
SynchronizedDataStore.this.synchronizer.endTransaction(this.readOnly);
synchronized (SynchronizedDataStore.this.transactions) {
SynchronizedDataStore.this.transactions.remove(this);
}
}
}
}
}