/*
* Copyright 2016 The Simple File Server Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sfs.filesystem;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.logging.Logger;
import io.vertx.core.streams.ReadStream;
import org.sfs.SfsVertx;
import org.sfs.io.Block;
import org.sfs.io.BufferEndableWriteStream;
import org.sfs.io.BufferWriteEndableWriteStream;
import org.sfs.rx.Defer;
import org.sfs.rx.ToVoid;
import rx.Observable;
import rx.exceptions.CompositeException;
import rx.functions.Func1;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
import static com.google.common.base.Optional.absent;
import static com.google.common.base.Optional.of;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.math.LongMath.checkedAdd;
import static io.vertx.core.buffer.Buffer.buffer;
import static io.vertx.core.logging.LoggerFactory.getLogger;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.WRITE;
import static java.util.Objects.hash;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.sfs.filesystem.BlobFile.Status.STOPPED;
import static org.sfs.io.Block.decodeFrame;
import static org.sfs.io.Block.encodeFrame;
import static org.sfs.math.Rounding.up;
import static org.sfs.protobuf.XVolume.XJournal.Header;
import static org.sfs.protobuf.XVolume.XJournal.Header.Builder;
import static org.sfs.protobuf.XVolume.XJournal.Header.parseFrom;
import static org.sfs.protobuf.XVolume.XJournal.Super;
import static org.sfs.protobuf.XVolume.XJournal.Super.newBuilder;
import static org.sfs.rx.Defer.aVoid;
import static org.sfs.rx.Defer.just;
import static org.sfs.util.ExceptionHelper.containsException;
import static rx.Observable.error;
public class JournalFile {
private static final Logger LOGGER = getLogger(JournalFile.class);
private static final long NOT_SET = -1;
private static final int SUPER_BLOCK_SIZE = 1024 * 1024;
private static final long SUPER_BLOCK_POSITION_0 = 0;
private static final long SUPER_BLOCK_POSITION_1 = 1024 * 1024;
private static final long SUPER_BLOCK_RESERVED = SUPER_BLOCK_SIZE * 2;
@VisibleForTesting
protected static final int DEFAULT_BLOCK_SIZE = 75;
private static final long DEFAULT_WRITE_STREAM_TIMEOUT = MINUTES.toMillis(1);
private BlobFile blobFile;
private int blockSize = (int) NOT_SET;
private long logStartPosition = NOT_SET;
private final AtomicReference<WritePosition> writePositionRef;
private final Path path;
public JournalFile(Path path, long writeStreamTimeout) {
this.path = path;
this.blobFile = new BlobFile(path, blockSize, writeStreamTimeout);
this.writePositionRef = new AtomicReference<>(null);
}
public JournalFile(Path path) {
this(path, DEFAULT_WRITE_STREAM_TIMEOUT);
}
public long firstLogEntryPosition() {
return logStartPosition;
}
public Observable<Void> disableWrites(SfsVertx vertx) {
return blobFile.disableWrites(vertx);
}
public Observable<Void> enableWrites(SfsVertx vertx) {
return blobFile.enableWrites(vertx);
}
public Observable<Long> size(SfsVertx vertx) {
return blobFile.size(vertx);
}
public Observable<Void> force(SfsVertx vertx, boolean metaData) {
return blobFile.force(vertx, metaData);
}
public int getBlockSize() {
return blockSize;
}
public Observable<Void> open(SfsVertx vertx) {
return aVoid()
.flatMap(aVoid -> {
// do some funcky stuff here to read the existing super block so that
// we have enough information to access the journal entries
BlobFile internalBlobFile = new BlobFile(path, SUPER_BLOCK_SIZE, DEFAULT_WRITE_STREAM_TIMEOUT);
return internalBlobFile.open(vertx, CREATE_NEW, READ, WRITE)
.flatMap(aVoid1 -> internalBlobFile.enableWrites(vertx))
.flatMap(aVoid1 -> {
Super superBlock =
newBuilder()
.setBlockSize(DEFAULT_BLOCK_SIZE)
.build();
return setSuperBlock(vertx, internalBlobFile, superBlock)
.map(aVoid11 -> superBlock);
})
.onErrorResumeNext(throwable -> {
if (containsException(FileAlreadyExistsException.class, throwable)) {
return internalBlobFile.close(vertx)
.flatMap(aVoid1 -> internalBlobFile.open(vertx, CREATE, READ, WRITE))
.flatMap(aVoid1 -> internalBlobFile.enableWrites(vertx))
.flatMap(aVoid1 -> getSuperBlock(vertx, internalBlobFile));
} else {
return error(throwable);
}
})
.doOnNext(superBlock -> {
blockSize = superBlock.getBlockSize();
logStartPosition = up(SUPER_BLOCK_RESERVED, blockSize);
})
.map(new ToVoid<>())
.flatMap(aVoid1 -> internalBlobFile.disableWrites(vertx))
.flatMap(aVoid1 -> internalBlobFile.force(vertx, true))
.onErrorResumeNext(throwable -> {
if (!STOPPED.equals(blobFile.getStatus())) {
return internalBlobFile.close(vertx)
.onErrorResumeNext(throwable1 -> {
return error(new CompositeException(throwable, throwable1));
});
} else {
return error(throwable);
}
})
.flatMap(aVoid1 -> internalBlobFile.close(vertx));
})
.flatMap(aVoid -> {
blobFile = new BlobFile(path, blockSize, DEFAULT_WRITE_STREAM_TIMEOUT);
return blobFile.open(vertx, CREATE, READ, WRITE);
});
}
private Observable<Void> setSuperBlock(SfsVertx vertx, BlobFile internalBlobFile, Super superBlock) {
Buffer buffer = buffer(superBlock.toByteArray());
Block.Frame<Buffer> frame = encodeFrame(buffer);
Buffer frameBuffer = frame.getData();
int frameSize = frameBuffer.length();
checkState(frameSize <= SUPER_BLOCK_SIZE, "Super block frame size was %s, which is greater block size of %s", frameSize, SUPER_BLOCK_SIZE);
// write the super block twice so that we can recover from a failed
// write
return aVoid()
.flatMap(aVoid -> internalBlobFile.consume(vertx, SUPER_BLOCK_POSITION_0, frameBuffer))
.flatMap(aVoid -> internalBlobFile.force(vertx, true))
.flatMap(aVoid -> internalBlobFile.consume(vertx, SUPER_BLOCK_POSITION_1, frameBuffer))
.flatMap(aVoid -> internalBlobFile.force(vertx, true));
}
private Observable<Super> getSuperBlock(SfsVertx vertx, BlobFile internalBlobFile) {
return aVoid()
.flatMap(aVoid -> getSuperBlock0(vertx, internalBlobFile, SUPER_BLOCK_POSITION_0))
.flatMap(superOptional -> {
if (superOptional.isPresent()) {
return just(superOptional);
} else {
return getSuperBlock0(vertx, internalBlobFile, SUPER_BLOCK_POSITION_1);
}
})
.doOnNext(superOptional -> checkState(superOptional.isPresent(), "Corrupt Super Block"))
.map(Optional::get);
}
private Observable<Optional<Super>> getSuperBlock0(SfsVertx vertx, BlobFile internalBlobFile, long position) {
return aVoid()
.doOnNext(aVoid -> checkState(position == SUPER_BLOCK_POSITION_0 || position == SUPER_BLOCK_POSITION_1, "Position must be equal to %s or %s", SUPER_BLOCK_POSITION_0, SUPER_BLOCK_POSITION_1))
.flatMap(aVoid -> {
BufferWriteEndableWriteStream bufferWriteStreamConsumer = new BufferWriteEndableWriteStream();
return internalBlobFile.produce(vertx, position, SUPER_BLOCK_SIZE, bufferWriteStreamConsumer)
.map(aVoid1 -> bufferWriteStreamConsumer.toBuffer());
})
.map(buffer -> {
Optional<Block.Frame<byte[]>> oFrame = decodeFrame(buffer, false);
if (oFrame.isPresent()) {
Block.Frame<byte[]> frame = oFrame.get();
return of(new ChecksummedPositional<byte[]>(position, frame.getData(), frame.getChecksum()) {
@Override
public boolean isChecksumValid() {
return frame.isChecksumValid();
}
});
} else {
return Optional.<ChecksummedPositional<byte[]>>absent();
}
})
.filter(Optional::isPresent)
.map(Optional::get)
.map(this::parseSuperBlock)
.filter(Optional::isPresent)
.map(Optional::get)
.map(headerChecksummedPositional -> {
Super header = headerChecksummedPositional.getValue();
return header;
})
.map(Optional::of)
.singleOrDefault(absent());
}
public Observable<Void> close(SfsVertx vertx) {
return blobFile.close(vertx);
}
public Observable<Long> append(SfsVertx vertx, Buffer metadata, Buffer data) {
int dataLength = data.length();
return append0(vertx, metadata, dataLength)
.flatMap(writePosition -> {
if (dataLength > 0) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Writing data @ position " + writePosition.getDataPosition());
}
return blobFile.consume(vertx, writePosition.getDataPosition(), data, false)
.doOnNext(aVoid -> {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Done writing data @ position " + writePosition.getDataPosition());
}
})
.map(aVoid -> writePosition.getHeaderPosition());
} else {
return Defer.just(writePosition.getHeaderPosition());
}
});
}
public Observable<Long> append(SfsVertx vertx, Buffer metadata, long dataLength, ReadStream<Buffer> data) {
return append0(vertx, metadata, dataLength)
.flatMap(writePosition -> {
if (dataLength > 0) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Writing data @ position " + writePosition.getDataPosition());
}
return blobFile.consume(vertx, writePosition.getDataPosition(), dataLength, data, false)
.doOnNext(aVoid -> {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Done writing data @ position " + writePosition.getDataPosition());
}
})
.map(aVoid -> writePosition.getHeaderPosition());
} else {
return Defer.just(writePosition.getHeaderPosition());
}
});
}
public Observable<Optional<Entry>> getFirstEntry(SfsVertx vertx) {
return getEntry(vertx, firstLogEntryPosition());
}
public Observable<Optional<Entry>> getEntry(SfsVertx vertx, long position) {
return aVoid()
.doOnNext(aVoid -> blobFile.checkOpen())
.doOnNext(aVoid -> checkLogEntryPosition(position))
.flatMap(aVoid -> {
BufferWriteEndableWriteStream bufferWriteStreamConsumer = new BufferWriteEndableWriteStream();
return blobFile.produce(vertx, position, blockSize, bufferWriteStreamConsumer)
.map(aVoid1 -> bufferWriteStreamConsumer.toBuffer());
})
.map(buffer -> {
Optional<Block.Frame<byte[]>> oFrame = decodeFrame(buffer, false);
if (oFrame.isPresent()) {
Block.Frame<byte[]> frame = oFrame.get();
return of(new ChecksummedPositional<byte[]>(position, frame.getData(), frame.getChecksum()) {
@Override
public boolean isChecksumValid() {
return frame.isChecksumValid();
}
});
} else {
return Optional.<ChecksummedPositional<byte[]>>absent();
}
})
.filter(Optional::isPresent)
.map(Optional::get)
.map(this::parseJournalHeader)
.filter(Optional::isPresent)
.map(Optional::get)
.map(headerChecksummedPositional -> {
Header header = headerChecksummedPositional.getValue();
return new Entry(blobFile, position, header);
})
.map(Optional::of)
.singleOrDefault(absent());
}
public Observable<Void> scanFromFirst(SfsVertx vertx, Func1<Entry, Observable<Boolean>> func) {
return aVoid()
.flatMap(aVoid -> {
JournalScanner journalScanner = new JournalScanner(this, firstLogEntryPosition());
return journalScanner.scan(vertx, func);
});
}
public Observable<Void> scan(SfsVertx vertx, long position, Func1<Entry, Observable<Boolean>> func) {
return aVoid()
.doOnNext(aVoid -> checkLogEntryPosition(position))
.flatMap(aVoid -> {
JournalScanner journalScanner = new JournalScanner(this, position);
return journalScanner.scan(vertx, func);
});
}
public Observable<Void> append(SfsVertx vertx, Buffer metadata) {
return append0(vertx, metadata, 0)
.map(new ToVoid<>());
}
protected Observable<WritePosition> append0(SfsVertx vertx, Buffer metadata, long dataLength) {
long metadataLength = metadata.length();
WritePosition writePosition = getAndIncWritePosition(metadataLength, dataLength);
long previousHeaderPosition = writePosition.getPreviousHeaderPosition();
long headerPosition = writePosition.getHeaderPosition();
long metadataPosition = writePosition.getMetadataPosition();
long dataPosition = writePosition.getDataPosition();
long nextHeaderPosition = writePosition.getNextHeaderPosition();
Builder headerBuilder =
Header.newBuilder()
.setNextHeaderPosition(nextHeaderPosition)
.setMetaDataPosition(metadataPosition)
.setMetaDataLength(metadataLength)
.setDataPosition(dataPosition)
.setDataLength(dataLength);
if (previousHeaderPosition >= 0) {
headerBuilder = headerBuilder.setPreviousHeaderPositon(previousHeaderPosition);
}
Buffer headerBuffer = buffer(headerBuilder.build().toByteArray());
Block.Frame<Buffer> headerFrame = encodeFrame(headerBuffer);
Buffer headerFrameBuffer = headerFrame.getData();
int headerFrameSize = headerFrameBuffer.length();
checkState(headerFrameSize <= blockSize, "Header Frame size was %s, which is greater block size of %s", headerFrameSize, blockSize);
return aVoid()
.doOnNext(aVoid -> {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Writing header frame @ position " + headerPosition);
}
})
.flatMap(aVoid -> blobFile.consume(vertx, headerPosition, headerFrameBuffer, true))
.doOnNext(aVoid -> {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Done Writing header frame @ position " + headerPosition);
}
})
.flatMap(aVoid -> {
if (metadataLength > 0) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Writing metadata @ position " + metadataPosition);
}
return blobFile.consume(vertx, metadataPosition, metadata, false)
.doOnNext(aVoid1 -> {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Done writing metadata @ position " + metadataPosition);
}
});
} else {
return aVoid();
}
})
.map(aVoid -> writePosition);
}
protected WritePosition getAndIncWritePosition(long metadataLength, long dataLength) {
while (true) {
WritePosition lastWritePosition = writePositionRef.get();
if (lastWritePosition == null) {
long headerPosition = firstLogEntryPosition();
long metadataPosition = checkedAdd(headerPosition, blockSize);
long dataPosition = checkedAdd(metadataPosition, metadataLength);
long nextHeaderPosition = up(checkedAdd(dataPosition, dataLength), blockSize);
WritePosition nextWritePosition = new WritePosition(NOT_SET, headerPosition, metadataPosition, dataPosition, nextHeaderPosition);
assertWritePosition(nextWritePosition, metadataLength, dataLength);
if (writePositionRef.compareAndSet(null, nextWritePosition)) {
return nextWritePosition;
}
} else {
WritePosition nextWritePosition = nextWritePosition(lastWritePosition, metadataLength, dataLength);
assertWritePosition(nextWritePosition, metadataLength, dataLength);
if (writePositionRef.compareAndSet(lastWritePosition, nextWritePosition)) {
return nextWritePosition;
}
}
}
}
private void checkLogEntryPosition(long position) {
checkArgument(position != NOT_SET, "logStartPosition has not been set");
checkArgument(position >= logStartPosition, "Position must be >= %s", logStartPosition);
}
private WritePosition nextWritePosition(WritePosition lastWritePosition, long metadataLength, long dataLength) {
long previousHeaderPosition = lastWritePosition.getHeaderPosition();
long headerPosition = lastWritePosition.getNextHeaderPosition();
long metadataPosition = checkedAdd(headerPosition, blockSize);
long dataPosition = checkedAdd(metadataPosition, metadataLength);
long nextHeaderPosition = up(checkedAdd(dataPosition, dataLength), blockSize);
return new WritePosition(previousHeaderPosition, headerPosition, metadataPosition, dataPosition, nextHeaderPosition);
}
protected void checkAligned(long value, int blockSize) {
checkState(value % blockSize == 0, "%s is not multiple of %s", value, blockSize);
}
protected void assertWritePosition(WritePosition writePosition, long metadataLength, long dataLength) {
checkState(blockSize != NOT_SET);
long previousHeaderPosition = writePosition.getPreviousHeaderPosition();
long headerPosition = writePosition.getHeaderPosition();
long metadataPosition = writePosition.getMetadataPosition();
long dataPosition = writePosition.getDataPosition();
long nextHeaderPosition = writePosition.getNextHeaderPosition();
// some sanity checks
if (previousHeaderPosition != NOT_SET) {
checkAligned(previousHeaderPosition, blockSize);
}
checkAligned(headerPosition, blockSize);
checkAligned(nextHeaderPosition, blockSize);
checkLogEntryPosition(headerPosition);
long expectedMetadataPosition = headerPosition + blockSize;
long expectedDataPosition = metadataPosition + metadataLength;
long expectedNextHeaderPosition = up(dataPosition + dataLength, blockSize);
checkState(metadataPosition == expectedMetadataPosition, "Metadata position was %s, expected %s", metadataPosition, expectedMetadataPosition);
checkState(dataPosition == expectedDataPosition, "Data position was %s, expected %s", dataPosition, expectedDataPosition);
checkState(nextHeaderPosition == expectedNextHeaderPosition, "Next header position was %s, expected %s", nextHeaderPosition, expectedNextHeaderPosition);
}
protected Optional<ChecksummedPositional<Header>> parseJournalHeader(ChecksummedPositional<byte[]> checksummedPositional) {
try {
if (checksummedPositional.isChecksumValid()) {
Header indexBlock = parseFrom(checksummedPositional.getValue());
return of(new ChecksummedPositional<Header>(checksummedPositional.getPosition(), indexBlock, checksummedPositional.getActualChecksum()) {
@Override
public boolean isChecksumValid() {
return true;
}
});
} else {
LOGGER.warn("Invalid checksum for index block @ position " + checksummedPositional.getPosition());
return absent();
}
} catch (Throwable e) {
LOGGER.warn("Error parsing index block @ position " + checksummedPositional.getPosition(), e);
return absent();
}
}
protected Optional<ChecksummedPositional<Super>> parseSuperBlock(ChecksummedPositional<byte[]> checksummedPositional) {
try {
if (checksummedPositional.isChecksumValid()) {
Super indexBlock = Super.parseFrom(checksummedPositional.getValue());
return of(new ChecksummedPositional<Super>(checksummedPositional.getPosition(), indexBlock, checksummedPositional.getActualChecksum()) {
@Override
public boolean isChecksumValid() {
return true;
}
});
} else {
LOGGER.warn("Invalid checksum for index block @ position " + checksummedPositional.getPosition());
return absent();
}
} catch (Throwable e) {
LOGGER.warn("Error parsing index block @ position " + checksummedPositional.getPosition(), e);
return absent();
}
}
public static class Entry {
private final long headerPosition;
private final Header header;
private final BlobFile blobFile;
public Entry(BlobFile blobFile, long headerPosition, Header header) {
this.blobFile = blobFile;
this.headerPosition = headerPosition;
this.header = header;
}
public long getHeaderPosition() {
return headerPosition;
}
public long getMetaDataPosition() {
return header.getMetaDataPosition();
}
public long getMetaDataLength() {
return header.getMetaDataLength();
}
public long getDataPosition() {
return header.getDataPosition();
}
public long getDataLength() {
return header.getDataLength();
}
public long getNextHeaderPosition() {
return header.getNextHeaderPosition();
}
public long getPreviousHeaderPositon() {
return header.getPreviousHeaderPositon();
}
public Observable<Buffer> getMetadata(SfsVertx vertx) {
long metadataLength = header.getMetaDataLength();
long metadataPosition = header.getMetaDataPosition();
BufferWriteEndableWriteStream bufferWriteStreamConsumer = new BufferWriteEndableWriteStream();
return blobFile.produce(vertx, metadataPosition, metadataLength, bufferWriteStreamConsumer)
.map(aVoid -> bufferWriteStreamConsumer.toBuffer());
}
public Observable<Void> produceData(SfsVertx vertx, BufferEndableWriteStream bufferStreamConsumer) {
long dataLength = header.getDataLength();
long dataPosition = header.getDataPosition();
return blobFile.produce(vertx, dataPosition, dataLength, bufferStreamConsumer);
}
}
private static class WritePosition {
private final long previousHeaderPosition;
private final long headerPosition;
private final long nextHeaderPosition;
private final long metadataPosition;
private final long dataPosition;
public WritePosition(long previousHeaderPosition, long headerPosition, long metadataPosition, long dataPosition, long nextHeaderPosition) {
this.previousHeaderPosition = previousHeaderPosition;
this.headerPosition = headerPosition;
this.nextHeaderPosition = nextHeaderPosition;
this.dataPosition = dataPosition;
this.metadataPosition = metadataPosition;
}
public long getMetadataPosition() {
return metadataPosition;
}
public long getPreviousHeaderPosition() {
return previousHeaderPosition;
}
public long getHeaderPosition() {
return headerPosition;
}
public long getNextHeaderPosition() {
return nextHeaderPosition;
}
public long getDataPosition() {
return dataPosition;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof WritePosition)) return false;
WritePosition that = (WritePosition) o;
return previousHeaderPosition == that.previousHeaderPosition &&
headerPosition == that.headerPosition &&
nextHeaderPosition == that.nextHeaderPosition &&
metadataPosition == that.metadataPosition &&
dataPosition == that.dataPosition;
}
@Override
public int hashCode() {
return hash(previousHeaderPosition, headerPosition, nextHeaderPosition, metadataPosition, dataPosition);
}
@Override
public String toString() {
return "WritePosition{" +
"previousHeaderPosition=" + previousHeaderPosition +
", headerPosition=" + headerPosition +
", metadataPosition=" + metadataPosition +
", dataPosition=" + dataPosition +
", nextHeaderPosition=" + nextHeaderPosition +
'}';
}
}
}