Package org.waveprotocol.box.server.waveserver

Source Code of org.waveprotocol.box.server.waveserver.WaveletContainerImpl

/**
* Copyright 2009 Google Inc.
*
* 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.waveprotocol.box.server.waveserver;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.InvalidProtocolBufferException;

import org.waveprotocol.box.common.DeltaSequence;
import org.waveprotocol.box.server.frontend.CommittedWaveletSnapshot;
import org.waveprotocol.box.server.persistence.PersistenceException;
import org.waveprotocol.box.server.util.WaveletDataUtil;
import org.waveprotocol.wave.federation.Proto.ProtocolAppliedWaveletDelta;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.OperationPair;
import org.waveprotocol.wave.model.operation.TransformException;
import org.waveprotocol.wave.model.operation.wave.Transform;
import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.ParticipantIdUtil;
import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
import org.waveprotocol.wave.model.wave.data.ReadableWaveletData;
import org.waveprotocol.wave.util.logging.Log;

import java.util.Collection;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* Contains the history of a wavelet - applied and transformed deltas plus the
* content of the wavelet.
*
* TODO(soren): Unload the wavelet (remove it from WaveMap) if it becomes
* corrupt or fails to load from storage.
*/
abstract class WaveletContainerImpl implements WaveletContainer {

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

  private static final int AWAIT_LOAD_TIMEOUT_SECONDS = 1000;

  protected enum State {
    /** Everything is working fine. */
    OK,

    /** Wavelet state is being loaded from storage. */
    LOADING,

    /** Wavelet has been deleted, the instance will not contain any data. */
    DELETED,

    /**
     * For some reason this instance is broken, e.g. a remote wavelet update
     * signature failed.
     */
    CORRUPTED
  }

  private final Executor storageContinuationExecutor;

  private final Lock readLock;
  private final ReentrantReadWriteLock.WriteLock writeLock;
  private final WaveletName waveletName;
  private final WaveletNotificationSubscriber notifiee;
  private final ParticipantId sharedDomainParticipantId;
  /** Is counted down when initial loading from storage completes. */
  private final CountDownLatch loadLatch = new CountDownLatch(1);
  /** Is set at most once, before loadLatch is counted down. */
  private WaveletState waveletState;
  private State state = State.LOADING;

  /**
   * Constructs an empty WaveletContainer for a wavelet.
   * WaveletData is not set until a delta has been applied.
   *
   * @param notifiee the subscriber to notify of wavelet updates and commits.
   * @param waveletState the wavelet's delta history and current state.
   * @param waveDomain the wave server domain.
   * @param storageContinuationExecutor the executor used to perform post wavelet loading logic.
   */
  public WaveletContainerImpl(WaveletName waveletName, WaveletNotificationSubscriber notifiee,
      final ListenableFuture<? extends WaveletState> waveletStateFuture, String waveDomain,
      Executor storageContinuationExecutor) {
    this.waveletName = waveletName;
    this.notifiee = notifiee;
    this.sharedDomainParticipantId =
        waveDomain != null ? ParticipantIdUtil.makeUnsafeSharedDomainParticipantId(waveDomain)
            : null;
    this.storageContinuationExecutor = storageContinuationExecutor;
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    this.readLock = readWriteLock.readLock();
    this.writeLock = readWriteLock.writeLock();

    waveletStateFuture.addListener(
        new Runnable() {
          @Override
          public void run() {
            acquireWriteLock();
            try {
              Preconditions.checkState(waveletState == null,
                  "Repeat attempts to set wavelet state");
              Preconditions.checkState(state == State.LOADING, "Unexpected state %s", state);
              waveletState = FutureUtil.getResultOrPropagateException(
                  waveletStateFuture, PersistenceException.class);
              Preconditions.checkState(waveletState.getWaveletName().equals(getWaveletName()),
                  "Wrong wavelet state, named %s, expected %s",
                  waveletState.getWaveletName(), getWaveletName());
              state = State.OK;
            } catch (PersistenceException e) {
              LOG.warning("Failed to load wavelet " + getWaveletName(), e);
              state = State.CORRUPTED;
            } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
              LOG.warning("Interrupted loading wavelet " + getWaveletName(), e);
              state = State.CORRUPTED;
            } catch (RuntimeException e) {
              // TODO(soren): would be better to terminate the process in this case
              LOG.severe("Unexpected exception loading wavelet " + getWaveletName(), e);
              state = State.CORRUPTED;
            } finally {
              releaseWriteLock();
            }
            loadLatch.countDown();
          }
        },
        storageContinuationExecutor);
  }

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

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

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

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

  protected void notifyOfDeltas(ImmutableList<WaveletDeltaRecord> deltas,
      ImmutableSet<String> domainsToNotify) {
    Preconditions.checkState(writeLock.isHeldByCurrentThread(), "must hold write lock");
    Preconditions.checkArgument(!deltas.isEmpty(), "empty deltas");
    HashedVersion endVersion = deltas.get(deltas.size() - 1).getResultingVersion();
    HashedVersion currentVersion = getCurrentVersion();
    Preconditions.checkArgument(endVersion.equals(currentVersion),
        "cannot notify of deltas ending in %s != current version %s", endVersion, currentVersion);
    notifiee.waveletUpdate(waveletState.getSnapshot(), deltas, domainsToNotify);
  }

  protected void notifyOfCommit(HashedVersion version, ImmutableSet<String> domainsToNotify) {
    Preconditions.checkState(writeLock.isHeldByCurrentThread(), "must hold write lock");
    notifiee.waveletCommitted(getWaveletName(), version, domainsToNotify);
  }

  /**
   * Blocks until the initial load of the wavelet state from storage completes.
   * Should be called without the read or write lock held.
   *
   * @throws WaveletStateException if the wavelet fails to load,
   *         either because of a storage access failure or timeout,
   *         or because the current thread is interrupted.
   */
  protected void awaitLoad() throws WaveletStateException {
    Preconditions.checkState(!writeLock.isHeldByCurrentThread(), "should not hold write lock");
    try {
      if (!loadLatch.await(AWAIT_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
        throw new WaveletStateException("Timed out waiting for wavelet to load");
      }
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new WaveletStateException("Interrupted waiting for wavelet to load");
    }
  }

  /**
   * Verifies that the wavelet is in an operational state (not loading,
   * not corrupt).
   *
   * Should be preceded by a call to awaitLoad() so that the initial load from
   * storage has completed. Should be called with the read or write lock held.
   *
   * @throws WaveletStateException if the wavelet is loading or marked corrupt.
   */
  protected void checkStateOk() throws WaveletStateException {
    if (state != State.OK) {
      throw new WaveletStateException("The wavelet is in an unusable state: " + state);
    }
  }

  /**
   * Flags the wavelet corrupted so future calls to checkStateOk() will fail.
   */
  protected void markStateCorrupted() {
    Preconditions.checkState(writeLock.isHeldByCurrentThread(), "must hold write lock");
    state = State.CORRUPTED;
  }

  protected void persist(final HashedVersion version, final ImmutableSet<String> domainsToNotify) {
    Preconditions.checkState(writeLock.isHeldByCurrentThread(), "must hold write lock");
    final ListenableFuture<Void> result = waveletState.persist(version);
    result.addListener(
        new Runnable() {
          @Override
          public void run() {
            try {
              result.get();
            } catch (InterruptedException e) {
              Thread.currentThread().interrupt();
            } catch (ExecutionException e) {
              LOG.severe("Version " + version, e);
            }
            acquireWriteLock();
            try {
              waveletState.flush(version);
              notifyOfCommit(version, domainsToNotify);
            } finally {
              releaseWriteLock();
            }
          }
        },
        storageContinuationExecutor);
  }

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

  @Override
  public boolean checkAccessPermission(ParticipantId participantId) throws WaveletStateException {
    awaitLoad();
    acquireReadLock();
    try {
      checkStateOk();
      // ParticipantId will be null if the user isn't logged in. A user who isn't logged in should
      // have access to public waves once they've been implemented.
      // If the wavelet is empty, everyone has access (to write the first delta).
      // TODO(soren): determine if off-domain participants should be denied access if empty
      ReadableWaveletData snapshot = waveletState.getSnapshot();
      return WaveletDataUtil.checkAccessPermission(snapshot, participantId, sharedDomainParticipantId);
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public HashedVersion getLastCommittedVersion() throws WaveletStateException {
    awaitLoad();
    acquireReadLock();
    try {
      checkStateOk();
      return waveletState.getLastPersistedVersion();
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public ObservableWaveletData copyWaveletData() throws WaveletStateException {
    awaitLoad();
    acquireReadLock();
    try {
      checkStateOk();
      return WaveletDataUtil.copyWavelet(waveletState.getSnapshot());
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public CommittedWaveletSnapshot getSnapshot() throws WaveletStateException {
    awaitLoad();
    acquireReadLock();
    try {
      checkStateOk();
      return new CommittedWaveletSnapshot(waveletState.getSnapshot(),
          waveletState.getLastPersistedVersion());
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public <T> T applyFunction(Function<ReadableWaveletData, T> function)
      throws WaveletStateException {
    awaitLoad();
    acquireReadLock();
    try {
      checkStateOk();
      return function.apply(waveletState.getSnapshot());
    } finally {
      releaseReadLock();
    }
  }

  /**
   * 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
   * @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 WaveletDelta maybeTransformSubmittedDelta(WaveletDelta delta)
      throws InvalidHashException, OperationException {
    HashedVersion targetVersion = delta.getTargetVersion();
    HashedVersion currentVersion = getCurrentVersion();
    if (targetVersion.equals(currentVersion)) {
      // Applied version is the same, we're submitting against head, don't need to do OT
      return delta;
    } else {
      // Not submitting against head, we need to do OT, but check the versions really are different
      if (targetVersion.getVersion() == currentVersion.getVersion()) {
        LOG.warning("Mismatched hash, expected " + currentVersion + ") but delta targets (" +
            targetVersion + ")");
        throw new InvalidHashException(currentVersion, targetVersion);
      } else {
        return transformSubmittedDelta(delta);
      }
    }
  }

  /**
   * Finds range of server deltas needed to transform against, then transforms all client
   * ops against the server ops.
   */
  private WaveletDelta transformSubmittedDelta(WaveletDelta submittedDelta)
      throws OperationException, InvalidHashException {
    HashedVersion targetVersion = submittedDelta.getTargetVersion();
    HashedVersion currentVersion = getCurrentVersion();
    Preconditions.checkArgument(!targetVersion.equals(currentVersion));
    DeltaSequence serverDeltas =
        waveletState.getTransformedDeltaHistory(targetVersion, currentVersion);
    if (serverDeltas == null) {
      LOG.warning("Attempt to apply delta at unknown hashed version " + targetVersion);
      throw new InvalidHashException(currentVersion, targetVersion);
    }
    Preconditions.checkState(!serverDeltas.isEmpty(),
        "No deltas between valid versions %s and %s", targetVersion, currentVersion);

    ParticipantId clientAuthor = submittedDelta.getAuthor();
    // TODO(anorth): remove this copy somehow; currently, it's necessary to
    // ensure that clientOps.equals() works correctly below (because
    // WaveletDelta breaks the List.equals() contract)
    List<WaveletOperation> clientOps = Lists.newArrayList(submittedDelta);
    for (TransformedWaveletDelta serverDelta : 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 WaveletDelta(clientAuthor, targetVersion, clientOps);
      }
      ParticipantId serverAuthor = serverDelta.getAuthor();
      if (clientAuthor.equals(serverAuthor) && clientOps.equals(serverDelta)) {
        // This is a duplicate of the server delta.
        return new WaveletDelta(clientAuthor, targetVersion, clientOps);
      }
      clientOps = transformOps(clientOps, serverDelta);
      targetVersion = serverDelta.getResultingVersion();
    }
    Preconditions.checkState(targetVersion.equals(currentVersion));
    return new WaveletDelta(clientAuthor, targetVersion, clientOps);
  }

  /**
   * 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 serverOps may be unmodifiable
   * @return transformed client ops
   */
  private List<WaveletOperation> transformOps(List<WaveletOperation> clientOps,
      List<WaveletOperation> serverOps) throws OperationException {
    List<WaveletOperation> transformedClientOps = Lists.newArrayList();

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

  /**
   * Builds a {@link WaveletDeltaRecord} and applies it to the wavelet container.
   * The delta must be non-empty.
   */
  protected WaveletDeltaRecord applyDelta(
      ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta, WaveletDelta transformed)
      throws InvalidProtocolBufferException, OperationException {
    TransformedWaveletDelta transformedDelta =
        AppliedDeltaUtil.buildTransformedDelta(appliedDelta, transformed);
    waveletState.appendDelta(transformed.getTargetVersion(), transformedDelta, appliedDelta);

    return new WaveletDeltaRecord(transformed.getTargetVersion(), appliedDelta, transformedDelta);
  }

  /**
   * @param versionActuallyAppliedAt the version to look up
   * @return the applied delta applied at the specified hashed version
   */
  protected ByteStringMessage<ProtocolAppliedWaveletDelta> lookupAppliedDelta(
      HashedVersion versionActuallyAppliedAt) {
    return waveletState.getAppliedDelta(versionActuallyAppliedAt);
  }

  /**
   * @param endVersion the version to look up
   * @return the applied delta with the given resulting version
   */
  protected ByteStringMessage<ProtocolAppliedWaveletDelta> lookupAppliedDeltaByEndVersion(
      HashedVersion endVersion) {
    return waveletState.getAppliedDeltaByEndVersion(endVersion);
  }

  protected TransformedWaveletDelta lookupTransformedDelta(HashedVersion appliedAtVersion) {
    return waveletState.getTransformedDelta(appliedAtVersion);
  }

  /**
   * @throws AccessControlException with the given message if version does not
   *         match a delta boundary in the wavelet history.
   */
  private void checkVersionIsDeltaBoundary(HashedVersion version, String message)
      throws AccessControlException {
    HashedVersion actual = waveletState.getHashedVersion(version.getVersion());
    if (!version.equals(actual)) {
      LOG.info("Unrecognized " + message + " at version " + version + ", actual " + actual);
      // We omit the hash from the message to avoid leaking it.
      throw new AccessControlException(
          "Unrecognized " + message + " at version " + version.getVersion());
    }
  }

  @Override
  public Collection<ByteStringMessage<ProtocolAppliedWaveletDelta>> requestHistory(
      HashedVersion startVersion, HashedVersion endVersion)
      throws AccessControlException, WaveletStateException {
    acquireReadLock();
    try {
      checkStateOk();
      checkVersionIsDeltaBoundary(startVersion, "start version");
      checkVersionIsDeltaBoundary(endVersion, "end version");
      return waveletState.getAppliedDeltaHistory(startVersion, endVersion);
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public Collection<TransformedWaveletDelta> requestTransformedHistory(HashedVersion startVersion,
      HashedVersion endVersion) throws AccessControlException, WaveletStateException {
    awaitLoad();
    acquireReadLock();
    try {
      checkStateOk();
      checkVersionIsDeltaBoundary(startVersion, "start version");
      checkVersionIsDeltaBoundary(endVersion, "end version");
      return waveletState.getTransformedDeltaHistory(startVersion, endVersion);
    } finally {
      releaseReadLock();
    }
  }

  @Override
  public boolean hasParticipant(ParticipantId participant) throws WaveletStateException {
    awaitLoad();
    acquireReadLock();
    try {
      checkStateOk();
      ReadableWaveletData snapshot = waveletState.getSnapshot();
      return snapshot != null && snapshot.getParticipants().contains(participant);
    } finally {
      releaseReadLock();
    }
  }



  @Override
  public ParticipantId getSharedDomainParticipant() {
    return sharedDomainParticipantId;
  }

  @Override
  public ParticipantId getCreator() {
    ReadableWaveletData snapshot = waveletState.getSnapshot();
    return snapshot != null ? snapshot.getCreator() : null;
  }

  @Override
  public boolean isEmpty() throws WaveletStateException {
    awaitLoad();
    acquireReadLock();
    try {
      checkStateOk();
      return waveletState.getSnapshot() == null;
    } finally {
      releaseReadLock();
    }
  }

  protected HashedVersion getCurrentVersion() {
    return waveletState.getCurrentVersion();
  }

  protected ReadableWaveletData accessSnapshot() {
    return waveletState.getSnapshot();
  }
}
TOP

Related Classes of org.waveprotocol.box.server.waveserver.WaveletContainerImpl

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.