package eu.fbk.knowledgestore.filestore; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import eu.fbk.knowledgestore.data.Stream; import eu.fbk.knowledgestore.runtime.Component; import eu.fbk.knowledgestore.runtime.Synchronizer; /** * A {@code FileStore} wrapper that synchronizes and enforces a proper access to an another * {@code FileStore}. * <p> * This wrapper provides the following guarantees with respect to external access to the wrapped * {@link FileStore}: * <ul> * <li>operations are started and committed according to the synchronization strategy enforced by * a supplied {@link Synchronizer};</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 the {@code FileStore} is closed, all pending {@code FileStore#list()} operations are * terminated.</li> * </ul> * </p> */ public final class SynchronizedFileStore extends ForwardingFileStore { private static final Logger LOGGER = LoggerFactory.getLogger(SynchronizedFileStore.class); private static final int NUM_LOCKS = 255; private static final int NEW = 0; private static final int INITIALIZED = 1; private static final int CLOSED = 2; private final FileStore delegate; private final Synchronizer synchronizer; private final AtomicInteger state; private final Object[] fileLocks; private final Set<Stream<String>> pendingListStreams; /** * Creates a new instance for the wrapped {@code FileStore} and the {@code Synchronizer} * specification string supplied. * * @param delegate * the wrapped {@code FileStore} * @param synchronizerSpec * the synchronizer specification string (see {@link Synchronizer}) */ public SynchronizedFileStore(final FileStore delegate, final String synchronizerSpec) { this.delegate = Preconditions.checkNotNull(delegate); this.synchronizer = Synchronizer.create(synchronizerSpec); this.state = new AtomicInteger(NEW); this.fileLocks = new Object[NUM_LOCKS]; for (int i = 0; i < NUM_LOCKS; ++i) { this.fileLocks[i] = new Object(); } this.pendingListStreams = Sets.newHashSet(); } @Override protected FileStore delegate() { return this.delegate; } private void checkState(final int expected) { final int state = this.state.get(); if (state != expected) { throw new IllegalStateException("FileStore " + (state == NEW ? "not initialized" : state == INITIALIZED ? "already initialized" : "already closed")); } } private Object lockFor(final String fileName) { return this.fileLocks[fileName.hashCode() % 0x7FFFFFFF % NUM_LOCKS]; } @Override public void init() throws IOException { checkState(NEW); super.init(); this.state.set(INITIALIZED); } @Override public InputStream read(final String fileName) throws FileMissingException, IOException { checkState(INITIALIZED); Preconditions.checkNotNull(fileName); this.synchronizer.beginTransaction(true); try { checkState(INITIALIZED); synchronized (lockFor(fileName)) { return super.read(fileName); } } finally { this.synchronizer.endTransaction(true); } } @Override public OutputStream write(final String fileName) throws FileExistsException, IOException { checkState(INITIALIZED); Preconditions.checkNotNull(fileName); this.synchronizer.beginTransaction(false); try { checkState(INITIALIZED); synchronized (lockFor(fileName)) { return super.write(fileName); } } finally { this.synchronizer.endTransaction(false); } } @Override public void delete(final String fileName) throws FileMissingException, IOException { checkState(INITIALIZED); Preconditions.checkNotNull(fileName); this.synchronizer.beginTransaction(false); try { checkState(INITIALIZED); synchronized (lockFor(fileName)) { super.delete(fileName); } } finally { this.synchronizer.endTransaction(false); } } @Override public Stream<String> list() throws IOException { checkState(INITIALIZED); this.synchronizer.beginTransaction(true); try { checkState(INITIALIZED); final Stream<String> stream = super.list(); synchronized (this.pendingListStreams) { this.pendingListStreams.add(stream); } stream.onClose(new Runnable() { @Override public void run() { SynchronizedFileStore.this.synchronizer.endTransaction(true); synchronized (SynchronizedFileStore.this.pendingListStreams) { SynchronizedFileStore.this.pendingListStreams.remove(this); } } }); return stream; } catch (final Throwable ex) { this.synchronizer.endTransaction(true); Throwables.propagateIfPossible(ex, IOException.class); throw Throwables.propagate(ex); } } @Override public void close() { if (!this.state.compareAndSet(INITIALIZED, CLOSED) && !this.state.compareAndSet(NEW, CLOSED)) { return; } List<Stream<String>> streamsToEnd; synchronized (this.pendingListStreams) { streamsToEnd = Lists.newArrayList(this.pendingListStreams); } try { for (final Stream<String> stream : streamsToEnd) { try { LOGGER.warn("Forcing closure of stream due to FileStore closure"); stream.close(); } catch (final Throwable ex) { LOGGER.error("Exception caught while closing stream: " + ex.getMessage(), ex); } } } finally { super.close(); } } }