Esempio n. 1
0
  /**
   * Apply a list of operations from a single delta to the wavelet container.
   *
   * @param ops to apply
   */
  protected void applyWaveletOperations(List<WaveletOperation> ops)
      throws OperationException, EmptyDeltaException {
    if (ops.isEmpty()) {
      LOG.warning("No operations to apply at version " + currentVersion);
      throw new EmptyDeltaException();
    }

    WaveletOperation lastOp = null;
    int opsApplied = 0;

    try {
      for (WaveletOperation op : ops) {
        lastOp = op;
        op.apply(waveletData);
        opsApplied++;
      }
    } catch (OperationException e) {
      LOG.warning(
          "Only applied "
              + opsApplied
              + " of "
              + ops.size()
              + " operations at version "
              + currentVersion
              + ", rolling back, failed op was "
              + lastOp,
          e);
      // Deltas are atomic, so roll back all operations that were successful
      rollbackWaveletOperations(ops.subList(0, opsApplied));
      throw new OperationException("Failed to apply all operations, none were applied", e);
    }
  }
Esempio n. 2
0
  @Override
  public NavigableSet<ByteStringMessage<ProtocolAppliedWaveletDelta>> requestHistory(
      ProtocolHashedVersion versionStart, ProtocolHashedVersion versionEnd)
      throws WaveletStateException {
    acquireReadLock();
    try {
      assertStateOk();
      // TODO: ### validate requested range.
      // TODO: #### make immutable.

      NavigableSet<ByteStringMessage<ProtocolAppliedWaveletDelta>> set =
          appliedDeltas.subSet(
              appliedDeltas.floor(emptyAppliedDeltaAtVersion(versionStart.getVersion())),
              true,
              emptyAppliedDeltaAtVersion(versionEnd.getVersion()),
              false);
      LOG.info(
          "### HR "
              + versionStart.getVersion()
              + " - "
              + versionEnd.getVersion()
              + " set - "
              + set.size()
              + " = "
              + set);
      return set;
    } finally {
      releaseReadLock();
    }
  }
Esempio n. 3
0
  /**
   * Finds range of server deltas needed to transform against, then transforms all client ops
   * against the server ops.
   */
  private VersionedWaveletDelta transformSubmittedDelta(
      WaveletDelta submittedDelta, HashedVersion appliedVersion)
      throws OperationException, InvalidHashException {

    NavigableSet<VersionedWaveletDelta> serverDeltas =
        deserializedTransformedDeltas.tailSet(
            deserializedTransformedDeltas.floor(
                emptyDeserializedDeltaAtVersion(appliedVersion.getVersion())),
            true);

    if (serverDeltas.size() == 0) {
      LOG.warning("Got empty server set, but not sumbitting to head! " + submittedDelta);
      // Not strictly an invalid hash, but it's a related issue
      throw new InvalidHashException("Cannot submit to head");
    }

    // Confirm that the target version/hash of this delta is valid.
    if (!serverDeltas.first().version.equals(appliedVersion)) {
      LOG.warning(
          "Mismatched hashes: expected: "
              + serverDeltas.first().version
              + " got: "
              + appliedVersion);
      // Don't leak the hash to the client in the error message.
      throw new InvalidHashException("Mismatched hashes at version " + appliedVersion.getVersion());
    }

    ParticipantId clientAuthor = submittedDelta.getAuthor();
    List<WaveletOperation> clientOps = submittedDelta.getOperations();
    for (VersionedWaveletDelta d : serverDeltas) {
      // If the client delta transforms to nothing before we've traversed all the server
      // deltas, return the version at which the delta was obliterated (rather than the
      // current version) to ensure that delta submission is idempotent.
      if (clientOps.isEmpty()) {
        return new VersionedWaveletDelta(new WaveletDelta(clientAuthor, clientOps), d.version);
      }
      ParticipantId serverAuthor = d.delta.getAuthor();
      List<WaveletOperation> serverOps = d.delta.getOperations();
      if (clientAuthor.equals(serverAuthor) && clientOps.equals(serverOps)) {
        return d;
      }
      clientOps = transformOps(clientOps, clientAuthor, serverOps, serverAuthor);
    }
    return new VersionedWaveletDelta(new WaveletDelta(clientAuthor, clientOps), currentVersion);
  }
Esempio n. 4
0
 /**
  * Transform a wavelet delta if it has been submitted against a different head (currentVersion).
  * Must be called with write lock held.
  *
  * @param delta to possibly transform
  * @param appliedVersion that the delta is applied to
  * @return the transformed delta and the version it was applied at (the version is the current
  *     version of the wavelet, unless the delta is a duplicate in which case it is the version at
  *     which it was originally applied)
  * @throws InvalidHashException if submitting against same version but different hash
  * @throws OperationException if transformation fails
  */
 protected VersionedWaveletDelta maybeTransformSubmittedDelta(
     WaveletDelta delta, HashedVersion appliedVersion)
     throws InvalidHashException, OperationException {
   if (appliedVersion.equals(currentVersion)) {
     // Applied version is the same, we're submitting against head, don't need to do OT
     return new VersionedWaveletDelta(delta, appliedVersion);
   } else {
     // Not submitting against head, we need to do OT, but check the versions really are different
     if (appliedVersion.getVersion() == currentVersion.getVersion()) {
       LOG.warning(
           "Same version ("
               + currentVersion.getVersion()
               + ") but different hashes ("
               + appliedVersion
               + "/"
               + currentVersion
               + ")");
       throw new InvalidHashException(
           "Different hash, same version: " + currentVersion.getVersion());
     } else {
       return transformSubmittedDelta(delta, appliedVersion);
     }
   }
 }
Esempio n. 5
0
/**
 * Contains the history of a wavelet - applied and transformed deltas plus the content of the
 * wavelet.
 */
abstract class WaveletContainerImpl implements WaveletContainer {

  /** A wavelet delta with a target hashed version. */
  protected static final class VersionedWaveletDelta {
    public final WaveletDelta delta;
    public final HashedVersion version;

    public VersionedWaveletDelta(WaveletDelta delta, HashedVersion version) {
      this.delta = delta;
      this.version = version;
    }
  }

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

  protected static final HashedVersionFactory HASHED_HISTORY_VERSION_FACTORY =
      new HashedVersionFactoryImpl();

  protected final NavigableSet<ByteStringMessage<ProtocolAppliedWaveletDelta>> appliedDeltas;
  protected final NavigableSet<ProtocolWaveletDelta> transformedDeltas;
  protected final NavigableSet<VersionedWaveletDelta> deserializedTransformedDeltas;
  private final Lock readLock;
  private final Lock writeLock;
  protected WaveletName waveletName;
  protected WaveletData waveletData;
  protected HashedVersion currentVersion;
  protected ProtocolHashedVersion lastCommittedVersion;
  protected State state;

  /** Constructor. */
  public WaveletContainerImpl(WaveletName waveletName) {
    this.waveletName = waveletName;
    waveletData = new WaveletDataImpl(waveletName.waveId, waveletName.waveletId);
    currentVersion = HASHED_HISTORY_VERSION_FACTORY.createVersionZero(waveletName);
    lastCommittedVersion = null;

    appliedDeltas = Sets.newTreeSet(appliedDeltaComparator);
    transformedDeltas = Sets.newTreeSet(transformedDeltaComparator);
    deserializedTransformedDeltas = Sets.newTreeSet(deserializedDeltaComparator);

    // Configure the locks used by this Wavelet.
    final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    readLock = readWriteLock.readLock();
    writeLock = readWriteLock.writeLock();
    state = State.OK;
  }

  protected void acquireReadLock() {
    readLock.lock();
  }

  protected void releaseReadLock() {
    readLock.unlock();
  }

  protected void acquireWriteLock() {
    writeLock.lock();
  }

  protected void releaseWriteLock() {
    writeLock.unlock();
  }

  /** A comparator to be used in a TreeSet for applied deltas. */
  protected static final Comparator<ByteStringMessage<ProtocolAppliedWaveletDelta>>
      appliedDeltaComparator =
          new Comparator<ByteStringMessage<ProtocolAppliedWaveletDelta>>() {
            @Override
            public int compare(
                ByteStringMessage<ProtocolAppliedWaveletDelta> first,
                ByteStringMessage<ProtocolAppliedWaveletDelta> second) {
              if (first == null && second != null) {
                return -1;
              }
              if (first != null && second == null) {
                return 1;
              }
              if (first == null && second == null) {
                return 0;
              }
              try {
                return Long.valueOf(getVersionAppliedAt(first.getMessage()).getVersion())
                    .compareTo(getVersionAppliedAt(second.getMessage()).getVersion());
              } catch (InvalidProtocolBufferException e) {
                throw new IllegalStateException("Invalid applied delta added to history", e);
              }
            }
          };

  /**
   * Return a dummy ProtocolWaveletDelta instance used as a range boundary for use in searches
   * within a NavigableSet of deltas.
   *
   * @param version the version to return the delta applied at
   * @return the generated dummy delta
   */
  private static ProtocolWaveletDelta emptyDeltaAtVersion(final long version) {
    return ProtocolWaveletDelta.newBuilder()
        .setAuthor("dummy")
        .setHashedVersion(WaveletOperationSerializer.serialize(HashedVersion.unsigned(version)))
        .build();
  }

  /** Return a dummy ProtocolAppliedWaveleetDelta instance used as a range boundary. */
  private static ByteStringMessage<ProtocolAppliedWaveletDelta> emptyAppliedDeltaAtVersion(
      final long version) {
    ProtocolAppliedWaveletDelta delta =
        ProtocolAppliedWaveletDelta.newBuilder()
            .setApplicationTimestamp(0)
            .setOperationsApplied(0)
            .setSignedOriginalDelta(
                ProtocolSignedDelta.newBuilder()
                    .setDelta(emptyDeltaAtVersion(version).toByteString()))
            .build();
    return ByteStringMessage.fromMessage(delta);
  }

  /**
   * Returns the version that the passed delta was actually applied at. Simply resolves to the
   * hashed version stored on the applied delta if it exists, or defaults to the hashed version
   * stored on the original immutable delta.
   *
   * @param appliedDelta the delta to examine
   * @return the hashed version applied at
   */
  protected static ProtocolHashedVersion getVersionAppliedAt(
      final ProtocolAppliedWaveletDelta appliedDelta) throws InvalidProtocolBufferException {
    if (appliedDelta.hasHashedVersionAppliedAt()) {
      return appliedDelta.getHashedVersionAppliedAt();
    } else {
      ProtocolSignedDelta signedDelta = appliedDelta.getSignedOriginalDelta();
      ProtocolWaveletDelta delta = ProtocolWaveletDelta.parseFrom(signedDelta.getDelta());
      return delta.getHashedVersion();
    }
  }

  /**
   * Return a dummy VersionedWaveletDelta instance used as a range boundary for use in searches
   * within a NavigableSet of deltas.
   *
   * @param version the version with which to return the versioned delta
   * @return a dummy versioned delta with a null delta
   */
  private static VersionedWaveletDelta emptyDeserializedDeltaAtVersion(final long version) {
    return new VersionedWaveletDelta(null, HashedVersion.unsigned(version));
  }

  /** A comparator to be used in a TreeSet for deserialized deltas. */
  private static final Comparator<VersionedWaveletDelta> deserializedDeltaComparator =
      new Comparator<VersionedWaveletDelta>() {
        @Override
        public int compare(VersionedWaveletDelta first, VersionedWaveletDelta second) {
          if (first == null && second != null) {
            return -1;
          }
          if (first != null && second == null) {
            return 1;
          }
          if (first == null && second == null) {
            return 0;
          }
          return Long.valueOf(first.version.getVersion()).compareTo(second.version.getVersion());
        }
      };

  /** A comparator to be used in a TreeSet for transformed deltas. */
  @VisibleForTesting
  static final Comparator<ProtocolWaveletDelta> transformedDeltaComparator =
      new Comparator<ProtocolWaveletDelta>() {
        @Override
        public int compare(ProtocolWaveletDelta first, ProtocolWaveletDelta second) {
          if (first == null && second != null) {
            return -1;
          }
          if (first != null && second == null) {
            return 1;
          }
          if (first == null && second == null) {
            return 0;
          }
          return Long.valueOf(first.getHashedVersion().getVersion())
              .compareTo(second.getHashedVersion().getVersion());
        }
      };

  protected void assertStateOk() throws WaveletStateException {
    if (state != State.OK) {
      throw new WaveletStateException(state, "The wavelet is not in a usable state. ");
    }
  }

  @Override
  public State getState() {
    acquireReadLock();
    try {
      return state;
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public void setState(State state) {
    acquireWriteLock();
    try {
      this.state = state;
    } finally {
      releaseWriteLock();
    }
  }

  @Override
  public boolean checkAccessPermission(ParticipantId participantId) throws WaveletStateException {
    acquireReadLock();
    try {
      assertStateOk();
      return waveletData.getParticipants().contains(participantId);
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public ProtocolHashedVersion getLastCommittedVersion() throws WaveletStateException {
    acquireReadLock();
    try {
      assertStateOk();
      return lastCommittedVersion;
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public WaveletData getWaveletData() {
    return waveletData;
  }

  @Override
  public <T> T getSnapshot(WaveletSnapshotBuilder<T> builder) {
    acquireWriteLock();
    try {
      return builder.build(waveletData, currentVersion, lastCommittedVersion);
    } finally {
      releaseWriteLock();
    }
  }

  /**
   * Transform a wavelet delta if it has been submitted against a different head (currentVersion).
   * Must be called with write lock held.
   *
   * @param delta to possibly transform
   * @param appliedVersion that the delta is applied to
   * @return the transformed delta and the version it was applied at (the version is the current
   *     version of the wavelet, unless the delta is a duplicate in which case it is the version at
   *     which it was originally applied)
   * @throws InvalidHashException if submitting against same version but different hash
   * @throws OperationException if transformation fails
   */
  protected VersionedWaveletDelta maybeTransformSubmittedDelta(
      WaveletDelta delta, HashedVersion appliedVersion)
      throws InvalidHashException, OperationException {
    if (appliedVersion.equals(currentVersion)) {
      // Applied version is the same, we're submitting against head, don't need to do OT
      return new VersionedWaveletDelta(delta, appliedVersion);
    } else {
      // Not submitting against head, we need to do OT, but check the versions really are different
      if (appliedVersion.getVersion() == currentVersion.getVersion()) {
        LOG.warning(
            "Same version ("
                + currentVersion.getVersion()
                + ") but different hashes ("
                + appliedVersion
                + "/"
                + currentVersion
                + ")");
        throw new InvalidHashException(
            "Different hash, same version: " + currentVersion.getVersion());
      } else {
        return transformSubmittedDelta(delta, appliedVersion);
      }
    }
  }

  /**
   * Apply a list of operations from a single delta to the wavelet container.
   *
   * @param ops to apply
   */
  protected void applyWaveletOperations(List<WaveletOperation> ops)
      throws OperationException, EmptyDeltaException {
    if (ops.isEmpty()) {
      LOG.warning("No operations to apply at version " + currentVersion);
      throw new EmptyDeltaException();
    }

    WaveletOperation lastOp = null;
    int opsApplied = 0;

    try {
      for (WaveletOperation op : ops) {
        lastOp = op;
        op.apply(waveletData);
        opsApplied++;
      }
    } catch (OperationException e) {
      LOG.warning(
          "Only applied "
              + opsApplied
              + " of "
              + ops.size()
              + " operations at version "
              + currentVersion
              + ", rolling back, failed op was "
              + lastOp,
          e);
      // Deltas are atomic, so roll back all operations that were successful
      rollbackWaveletOperations(ops.subList(0, opsApplied));
      throw new OperationException("Failed to apply all operations, none were applied", e);
    }
  }

  /**
   * Like applyWaveletOperations, but applies the inverse of the given operations (in reverse), and
   * no operations are permitted to fail.
   *
   * @param ops to roll back
   */
  private void rollbackWaveletOperations(List<WaveletOperation> ops) {
    for (int i = ops.size() - 1; i >= 0; i--) {
      try {
        ops.get(i).getInverse().apply(waveletData);
      } catch (OperationException e) {
        throw new IllegalArgumentException(
            "Failed to roll back " + ops.get(i) + " with inverse " + ops.get(i).getInverse(), e);
      }
    }
  }

  /**
   * Finds range of server deltas needed to transform against, then transforms all client ops
   * against the server ops.
   */
  private VersionedWaveletDelta transformSubmittedDelta(
      WaveletDelta submittedDelta, HashedVersion appliedVersion)
      throws OperationException, InvalidHashException {

    NavigableSet<VersionedWaveletDelta> serverDeltas =
        deserializedTransformedDeltas.tailSet(
            deserializedTransformedDeltas.floor(
                emptyDeserializedDeltaAtVersion(appliedVersion.getVersion())),
            true);

    if (serverDeltas.size() == 0) {
      LOG.warning("Got empty server set, but not sumbitting to head! " + submittedDelta);
      // Not strictly an invalid hash, but it's a related issue
      throw new InvalidHashException("Cannot submit to head");
    }

    // Confirm that the target version/hash of this delta is valid.
    if (!serverDeltas.first().version.equals(appliedVersion)) {
      LOG.warning(
          "Mismatched hashes: expected: "
              + serverDeltas.first().version
              + " got: "
              + appliedVersion);
      // Don't leak the hash to the client in the error message.
      throw new InvalidHashException("Mismatched hashes at version " + appliedVersion.getVersion());
    }

    ParticipantId clientAuthor = submittedDelta.getAuthor();
    List<WaveletOperation> clientOps = submittedDelta.getOperations();
    for (VersionedWaveletDelta d : serverDeltas) {
      // If the client delta transforms to nothing before we've traversed all the server
      // deltas, return the version at which the delta was obliterated (rather than the
      // current version) to ensure that delta submission is idempotent.
      if (clientOps.isEmpty()) {
        return new VersionedWaveletDelta(new WaveletDelta(clientAuthor, clientOps), d.version);
      }
      ParticipantId serverAuthor = d.delta.getAuthor();
      List<WaveletOperation> serverOps = d.delta.getOperations();
      if (clientAuthor.equals(serverAuthor) && clientOps.equals(serverOps)) {
        return d;
      }
      clientOps = transformOps(clientOps, clientAuthor, serverOps, serverAuthor);
    }
    return new VersionedWaveletDelta(new WaveletDelta(clientAuthor, clientOps), currentVersion);
  }

  /**
   * Transforms the specified client operations against the specified server operations, returning
   * the transformed client operations in a new list.
   *
   * @param clientOps may be unmodifiable
   * @param clientAuthor
   * @param serverOps may be unmodifiable
   * @param serverAuthor
   * @return The transformed client ops
   */
  private List<WaveletOperation> transformOps(
      List<WaveletOperation> clientOps,
      ParticipantId clientAuthor,
      List<WaveletOperation> serverOps,
      ParticipantId serverAuthor)
      throws OperationException {
    List<WaveletOperation> transformedClientOps = new ArrayList<WaveletOperation>();

    for (WaveletOperation c : clientOps) {
      for (WaveletOperation s : serverOps) {
        OperationPair<WaveletOperation> pair;
        try {
          pair = Transform.transform(c, clientAuthor, s, serverAuthor);
        } catch (TransformException e) {
          throw new OperationException(e);
        }
        c = pair.clientOp();
      }
      transformedClientOps.add(c);
    }
    return transformedClientOps;
  }

  /**
   * Commit an applied delta to this wavelet container.
   *
   * @param appliedDelta to commit
   * @param transformedDelta of the applied delta
   * @return result of the application
   */
  protected DeltaApplicationResult commitAppliedDelta(
      ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta, WaveletDelta transformedDelta) {

    ProtocolWaveletDelta transformedProtocolDelta =
        WaveletOperationSerializer.serialize(transformedDelta, currentVersion);
    transformedDeltas.add(transformedProtocolDelta);
    deserializedTransformedDeltas.add(new VersionedWaveletDelta(transformedDelta, currentVersion));
    appliedDeltas.add(appliedDelta);

    HashedVersion newVersion =
        HASHED_HISTORY_VERSION_FACTORY.create(
            appliedDelta.getByteArray(), currentVersion, transformedDelta.getOperations().size());
    currentVersion = newVersion;

    return new DeltaApplicationResult(
        appliedDelta, transformedProtocolDelta, WaveletOperationSerializer.serialize(newVersion));
  }

  /**
   * Returns the applied delta that was applied at a given hashed version.
   *
   * @param version the version to look up
   * @return the applied delta applied at the specified hashed version
   */
  protected ByteStringMessage<ProtocolAppliedWaveletDelta> lookupAppliedDelta(
      HashedVersion version) {
    return appliedDeltas.floor(emptyAppliedDeltaAtVersion(version.getVersion()));
  }

  @Override
  public NavigableSet<ByteStringMessage<ProtocolAppliedWaveletDelta>> requestHistory(
      ProtocolHashedVersion versionStart, ProtocolHashedVersion versionEnd)
      throws WaveletStateException {
    acquireReadLock();
    try {
      assertStateOk();
      // TODO: ### validate requested range.
      // TODO: #### make immutable.

      NavigableSet<ByteStringMessage<ProtocolAppliedWaveletDelta>> set =
          appliedDeltas.subSet(
              appliedDeltas.floor(emptyAppliedDeltaAtVersion(versionStart.getVersion())),
              true,
              emptyAppliedDeltaAtVersion(versionEnd.getVersion()),
              false);
      LOG.info(
          "### HR "
              + versionStart.getVersion()
              + " - "
              + versionEnd.getVersion()
              + " set - "
              + set.size()
              + " = "
              + set);
      return set;
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public NavigableSet<ProtocolWaveletDelta> requestTransformedHistory(
      ProtocolHashedVersion versionStart, ProtocolHashedVersion versionEnd)
      throws WaveletStateException {
    acquireReadLock();
    try {
      assertStateOk();
      // TODO: ### validate requested range.
      // TODO: #### make immutable.
      return transformedDeltas.subSet(
          transformedDeltas.floor(emptyDeltaAtVersion(versionStart.getVersion())),
          true,
          emptyDeltaAtVersion(versionEnd.getVersion()),
          false);
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public List<ParticipantId> getParticipants() {
    acquireReadLock();
    try {
      return (waveletData != null ? waveletData.getParticipants() : null);
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public HashedVersion getCurrentVersion() {
    return currentVersion;
  }
}