Package org.waveprotocol.box.server.persistence.file

Source Code of org.waveprotocol.box.server.persistence.file.FileDeltaCollection$DeltaHeader

/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you 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.waveprotocol.box.server.persistence.file;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;

import org.waveprotocol.box.server.persistence.PersistenceException;
import org.waveprotocol.box.server.persistence.protos.ProtoDeltaStoreDataSerializer;
import org.waveprotocol.box.server.persistence.protos.ProtoDeltaStoreData.ProtoTransformedWaveletDelta;
import org.waveprotocol.box.server.waveserver.AppliedDeltaUtil;
import org.waveprotocol.box.server.waveserver.ByteStringMessage;
import org.waveprotocol.box.server.waveserver.WaveletDeltaRecord;
import org.waveprotocol.box.server.waveserver.DeltaStore.DeltasAccess;
import org.waveprotocol.wave.federation.Proto.ProtocolAppliedWaveletDelta;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
import org.waveprotocol.wave.model.util.Pair;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.util.logging.Log;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;

/**
* A flat file based implementation of DeltasAccess. This class provides a storage backend for the
* deltas in a single wavelet.
*
* The file starts with a header. The header contains the version of the file protocol. After the
* version, the file contains a sequence of delta records. Each record contains a header followed
* by a WaveletDeltaRecord.
*
* A particular FileDeltaCollection instance assumes that it's <em>the only one</em> reading and
* writing a particular wavelet. The methods are <em>not</em> multithread-safe.
*
* See this document for design specifics:
* https://sites.google.com/a/waveprotocol.org/wave-protocol/protocol/design-proposals/wave-store-design-for-wave-in-a-box
*
* @author josephg@gmail.com (Joseph Gentle)
*/
public class FileDeltaCollection implements DeltasAccess {
  public static final String DELTAS_FILE_SUFFIX = ".deltas";
  public static final String INDEX_FILE_SUFFIX = ".index";

  private static final byte[] FILE_MAGIC_BYTES = new byte[]{'W', 'A', 'V', 'E'};
  private static final int FILE_PROTOCOL_VERSION = 1;
  private static final int FILE_HEADER_LENGTH = 8;

  private static final int DELTA_PROTOCOL_VERSION = 1;

  private static final Log LOG = Log.get(FileDeltaCollection.class);

  private final WaveletName waveletName;
  private final RandomAccessFile file;
  private final DeltaIndex index;

  private HashedVersion endVersion;
  private boolean isOpen;

  /**
   * A single record in the delta file.
   */
  private class DeltaHeader {
    /** Length in bytes of the header */
    public static final int HEADER_LENGTH = 12;

    /** The protocol version of the remaining fields. For now, must be 1. */
    public final int protoVersion;

    /** The length of the applied delta segment, in bytes. */
    public final int appliedDeltaLength;
    public final int transformedDeltaLength;

    public DeltaHeader(int protoVersion, int appliedDeltaLength, int transformedDeltaLength) {
      this.protoVersion = protoVersion;
      this.appliedDeltaLength = appliedDeltaLength;
      this.transformedDeltaLength = transformedDeltaLength;
    }

    public void checkVersion() throws IOException {
      if (protoVersion != DELTA_PROTOCOL_VERSION) {
        throw new IOException("Invalid delta header");
      }
    }
  }

  /**
   * Opens a file delta collection.
   *
   * @param waveletName name of the wavelet to open
   * @param basePath base path of files
   * @return an open collection
   * @throws IOException
   */
  public static FileDeltaCollection open(WaveletName waveletName, String basePath)
      throws IOException {
    Preconditions.checkNotNull(waveletName, "null wavelet name");

    RandomAccessFile deltaFile = FileUtils.getOrCreateFile(deltasFile(basePath, waveletName));
    setOrCheckFileHeader(deltaFile);
    DeltaIndex index = new DeltaIndex(indexFile(basePath, waveletName));

    FileDeltaCollection collection = new FileDeltaCollection(waveletName, deltaFile, index);

    index.openForCollection(collection);
    collection.initializeEndVersionAndTruncateTrailingJunk();
    return collection;
  }

  /**
   * Delete the delta files from disk.
   *
   * @throws PersistenceException
   */
  public static void delete(WaveletName waveletName, String basePath) throws PersistenceException {
    String error = "";
    File deltas = deltasFile(basePath, waveletName);

    if (deltas.exists()) {
      if (!deltas.delete()) {
        error += "Could not delete deltas file: " + deltas.getAbsolutePath() + ". ";
      }
    }

    File index = indexFile(basePath, waveletName);
    if (index.exists()) {
      if (!index.delete()) {
        error += "Could not delete index file: " + index.getAbsolutePath();
      }
    }
    if (!error.isEmpty()) {
      throw new PersistenceException(error);
    }
  }

  /**
   * Create a new file delta collection for the given wavelet.
   *
   * @param waveletName name of the wavelet
   * @param deltaFile the file of deltas
   * @param index index into deltas
   */
  public FileDeltaCollection(WaveletName waveletName, RandomAccessFile deltaFile,
      DeltaIndex index) {
    this.waveletName = waveletName;
    this.file = deltaFile;
    this.index = index;
    this.isOpen = true;
  }

  @Override
  public WaveletName getWaveletName() {
    return waveletName;
  }

  @Override
  public HashedVersion getEndVersion() {
    return endVersion;
  }

  @Override
  public WaveletDeltaRecord getDelta(long version) throws IOException {
    checkIsOpen();
    return seekToRecord(version) ? readRecord() : null;
  }

  @Override
  public WaveletDeltaRecord getDeltaByEndVersion(long version) throws IOException {
    checkIsOpen();
    return seekToEndRecord(version) ? readRecord() : null;
  }

  @Override
  public ByteStringMessage<ProtocolAppliedWaveletDelta> getAppliedDelta(long version)
      throws IOException {
    checkIsOpen();
    return seekToRecord(version) ? readAppliedDeltaFromRecord() : null;
  }

  @Override
  public TransformedWaveletDelta getTransformedDelta(long version) throws IOException {
    checkIsOpen();
    return seekToRecord(version) ? readTransformedDeltaFromRecord() : null;
  }

  @Override
  public HashedVersion getAppliedAtVersion(long version) throws IOException {
    checkIsOpen();
    ByteStringMessage<ProtocolAppliedWaveletDelta> applied = getAppliedDelta(version);

    return (applied != null) ? AppliedDeltaUtil.getHashedVersionAppliedAt(applied) : null;
  }

  @Override
  public HashedVersion getResultingVersion(long version) throws IOException {
    checkIsOpen();
    TransformedWaveletDelta transformed = getTransformedDelta(version);

    return (transformed != null) ? transformed.getResultingVersion() : null;
  }

  @Override
  public void close() throws IOException {
    file.close();
    index.close();
    endVersion = null;
    isOpen = false;
  }

  @Override
  public void append(Collection<WaveletDeltaRecord> deltas) throws PersistenceException {
    checkIsOpen();
    try {
      file.seek(file.length());

      WaveletDeltaRecord lastDelta = null;
      for (WaveletDeltaRecord delta : deltas) {
        index.addDelta(delta.getTransformedDelta().getAppliedAtVersion(),
            delta.getTransformedDelta().size(),
            file.getFilePointer());
        writeDelta(delta);
        lastDelta = delta;
      }

      // fsync() before returning.
      file.getChannel().force(true);
      endVersion = lastDelta.getTransformedDelta().getResultingVersion();
    } catch (IOException e) {
      throw new PersistenceException(e);
    }
  }

  @Override
  public boolean isEmpty() {
    checkIsOpen();
    return index.length() == 0;
  }

  /**
   * Creates a new iterator to move over the positions of the deltas in the file.
   *
   * Each pair returned is ((version, numOperations), offset).
   * @throws IOException
   */
  Iterable<Pair<Pair<Long,Integer>, Long>> getOffsetsIterator() throws IOException {
    checkIsOpen();

    return new Iterable<Pair<Pair<Long, Integer>, Long>>() {
      @Override
      public Iterator<Pair<Pair<Long, Integer>, Long>> iterator() {
        return new Iterator<Pair<Pair<Long, Integer>, Long>>() {
          Pair<Pair<Long, Integer>, Long> nextRecord;
          long nextPosition = FILE_HEADER_LENGTH;

          @Override
          public void remove() {
            throw new UnsupportedOperationException();
          }

          @Override
          public Pair<Pair<Long, Integer>, Long> next() {
            Pair<Pair<Long, Integer>, Long> record = nextRecord;
            nextRecord = null;
            return record;
          }

          @Override
          public boolean hasNext() {
            // We're using hasNext to prime the next call to next(). This works because in practice
            // any call to next() is preceeded by at least one call to hasNext().
            // We need to actually read the record here because hasNext() should return false
            // if there's any incomplete data at the end of the file.
            try {
              if (file.length() <= nextPosition) {
                // End of file.
                return false;
              }
            } catch (IOException e) {
              throw new RuntimeException("Could not get file position", e);
            }

            if (nextRecord == null) {
              // Read the next record
              try {
                file.seek(nextPosition);
                TransformedWaveletDelta transformed = readTransformedDeltaFromRecord();
                nextRecord = Pair.of(Pair.of(transformed.getAppliedAtVersion(),
                        transformed.size()), nextPosition);
                nextPosition = file.getFilePointer();
              } catch (IOException e) {
                // The next entry is invalid. There was probably a write error / crash.
                LOG.severe("Error reading delta file for " + waveletName + " starting at " +
                    nextPosition, e);
                return false;
              }
            }

            return true;
          }
        };
      }
    };
  }

  @VisibleForTesting
  static final File deltasFile(String basePath, WaveletName waveletName) {
    String waveletPathPrefix = FileUtils.waveletNameToPathSegment(waveletName);
    return new File(basePath, waveletPathPrefix + DELTAS_FILE_SUFFIX);
  }

  @VisibleForTesting
  static final File indexFile(String basePath, WaveletName waveletName) {
    String waveletPathPrefix = FileUtils.waveletNameToPathSegment(waveletName);
    return new File(basePath, waveletPathPrefix + INDEX_FILE_SUFFIX);
  }

  /**
   * Checks that a file has a valid deltas header, adding the header if the
   * file is shorter than the header.
   */
  private static void setOrCheckFileHeader(RandomAccessFile file) throws IOException {
    Preconditions.checkNotNull(file);
    file.seek(0);

    if (file.length() < FILE_HEADER_LENGTH) {
      // The file is new. Insert a header.
      file.write(FILE_MAGIC_BYTES);
      file.writeInt(FILE_PROTOCOL_VERSION);
    } else {
      byte[] magic = new byte[4];
      file.readFully(magic);
      if (!Arrays.equals(FILE_MAGIC_BYTES, magic)) {
        throw new IOException("Delta file magic bytes are incorrect");
      }

      int version = file.readInt();
      if (version != FILE_PROTOCOL_VERSION) {
        throw new IOException(String.format("File protocol version mismatch - expected %d got %d",
            FILE_PROTOCOL_VERSION, version));
      }
    }
  }

  private void checkIsOpen() {
    Preconditions.checkState(isOpen, "Delta collection closed");
  }

  /**
   * Seek to the start of a delta record. Returns false if the record doesn't exist.
   */
  private boolean seekToRecord(long version) throws IOException {
    Preconditions.checkArgument(version >= 0, "Version can't be negative");
    long offset = index.getOffsetForVersion(version);
    return seekTo(offset);
  }

  /**
   * Seek to the start of a delta record given its end version.
   * Returns false if the record doesn't exist.
   */
  private boolean seekToEndRecord(long version) throws IOException {
    Preconditions.checkArgument(version >= 0, "Version can't be negative");
    long offset = index.getOffsetForEndVersion(version);
    return seekTo(offset);
  }

  private boolean seekTo(long offset) throws IOException {
    if (offset == DeltaIndex.NO_RECORD_FOR_VERSION) {
      // There's no record for the specified version.
      return false;
    } else {
      file.seek(offset);
      return true;
    }
  }

  /**
   * Read a record and return it.
   */
  private WaveletDeltaRecord readRecord() throws IOException {
    DeltaHeader header = readDeltaHeader();

    ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta =
        readAppliedDelta(header.appliedDeltaLength);
    TransformedWaveletDelta transformedDelta = readTransformedWaveletDelta(
        header.transformedDeltaLength);

    return new WaveletDeltaRecord(AppliedDeltaUtil.getHashedVersionAppliedAt(appliedDelta),
        appliedDelta, transformedDelta);
  }

  /**
   * Reads a record, and only parses & returns the applied data field.
   */
  private ByteStringMessage<ProtocolAppliedWaveletDelta> readAppliedDeltaFromRecord()
      throws IOException {
    DeltaHeader header = readDeltaHeader();

    ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta =
        readAppliedDelta(header.appliedDeltaLength);
    file.skipBytes(header.transformedDeltaLength);

    return appliedDelta;
  }

  /**
   * Reads a record, and only parses & returns the transformed data field.
   */
  private TransformedWaveletDelta readTransformedDeltaFromRecord() throws IOException {
    DeltaHeader header = readDeltaHeader();

    file.skipBytes(header.appliedDeltaLength);
    TransformedWaveletDelta transformedDelta = readTransformedWaveletDelta(
        header.transformedDeltaLength);

    return transformedDelta;
  }


  // *** Low level data reading methods

  /** Read a header from the file. Does not move the file pointer before reading. */
  private DeltaHeader readDeltaHeader() throws IOException {
    int version = file.readInt();
    if (version != DELTA_PROTOCOL_VERSION) {
      throw new IOException("Delta header invalid");
    }
    int appliedDeltaLength = file.readInt();
    int transformedDeltaLength = file.readInt();
    DeltaHeader deltaHeader = new DeltaHeader(version, appliedDeltaLength, transformedDeltaLength);
    deltaHeader.checkVersion();
    // Verify the file size.
    long remaining = file.length() - file.getFilePointer();
    long missing = (appliedDeltaLength + transformedDeltaLength) - remaining;
    if (missing > 0) {
      throw new IOException("File is corrupted, missing " + missing + " bytes");
    }
    return deltaHeader;
  }

  /**
   * Write a header to the current location in the file
   */
  private void writeDeltaHeader(DeltaHeader header) throws IOException {
    file.writeInt(header.protoVersion);
    file.writeInt(header.appliedDeltaLength);
    file.writeInt(header.transformedDeltaLength);
  }

  /**
   * Read the applied delta at the current file position. After method call,
   * file position is directly after applied delta field.
   */
  private ByteStringMessage<ProtocolAppliedWaveletDelta> readAppliedDelta(int length)
      throws IOException {
    if (length == 0) {
      return null;
    }

    byte[] bytes = new byte[length];
    file.readFully(bytes);
    try {
      return ByteStringMessage.parseProtocolAppliedWaveletDelta(ByteString.copyFrom(bytes));
    } catch (InvalidProtocolBufferException e) {
      throw new IOException(e);
    }
  }

  /**
   * Write an applied delta to the current position in the file. Returns number of bytes written.
   */
  private int writeAppliedDelta(ByteStringMessage<ProtocolAppliedWaveletDelta> delta)
      throws IOException {
    if (delta != null) {
      byte[] bytes = delta.getByteArray();
      file.write(bytes);
      return bytes.length;
    } else {
      return 0;
    }
  }

  /**
   * Read a {@link TransformedWaveletDelta} from the current location in the file.
   */
  private TransformedWaveletDelta readTransformedWaveletDelta(int transformedDeltaLength)
      throws IOException {
    if(transformedDeltaLength < 0) {
      throw new IOException("Invalid delta length");
    }

    byte[] bytes = new byte[transformedDeltaLength];
    file.readFully(bytes);
    ProtoTransformedWaveletDelta delta;
    try {
      delta = ProtoTransformedWaveletDelta.parseFrom(bytes);
    } catch (InvalidProtocolBufferException e) {
      throw new IOException(e);
    }
    return ProtoDeltaStoreDataSerializer.deserialize(delta);
  }

  /**
   * Write a {@link TransformedWaveletDelta} to the file at the current location.
   * @return length of written data
   */
  private int writeTransformedWaveletDelta(TransformedWaveletDelta delta) throws IOException {
    long startingPosition = file.getFilePointer();
    ProtoTransformedWaveletDelta protoDelta = ProtoDeltaStoreDataSerializer.serialize(delta);
    OutputStream stream = Channels.newOutputStream(file.getChannel());
    protoDelta.writeTo(stream);
    return (int) (file.getFilePointer() - startingPosition);
  }

  /**
   * Read a delta to the file. Does not move the file pointer before writing. Returns number of
   * bytes written.
   */
  private long writeDelta(WaveletDeltaRecord delta) throws IOException {
    // We'll write zeros in place of the header and come back & write it at the end.
    long headerPointer = file.getFilePointer();
    file.write(new byte[DeltaHeader.HEADER_LENGTH]);

    int appliedLength = writeAppliedDelta(delta.getAppliedDelta());
    int transformedLength = writeTransformedWaveletDelta(delta.getTransformedDelta());

    long endPointer = file.getFilePointer();
    file.seek(headerPointer);
    writeDeltaHeader(new DeltaHeader(DELTA_PROTOCOL_VERSION, appliedLength, transformedLength));
    file.seek(endPointer);

    return endPointer - headerPointer;
  }

  /**
   * Reads the last complete record in the deltas file and truncates any trailing junk.
   */
  private void initializeEndVersionAndTruncateTrailingJunk() throws IOException {
    long numRecords = index.length();
    if (numRecords >= 1) {
      endVersion = getDeltaByEndVersion(numRecords).getResultingVersion();
    } else {
      endVersion = null;
    }
    // The file's position should be at the end. Truncate any
    // trailing junk such as from a partially completed write.
    file.setLength(file.getFilePointer());
  }
}
TOP

Related Classes of org.waveprotocol.box.server.persistence.file.FileDeltaCollection$DeltaHeader

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.