org.waveprotocol.box.server.waveserver.WaveletContainerImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.waveprotocol.box.server.waveserver.WaveletContainerImpl.java

Source

/**
 * 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.waveserver;

import org.waveprotocol.box.common.Receiver;
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.box.common.ListReceiver;
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.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;

import javax.annotation.Nullable;

/**
 * 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));
        ListReceiver<TransformedWaveletDelta> receiver = new ListReceiver<TransformedWaveletDelta>();
        waveletState.getTransformedDeltaHistory(targetVersion, currentVersion, receiver);
        DeltaSequence serverDeltas = DeltaSequence.of(receiver);
        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);

        WaveletDeltaRecord deltaRecord = new WaveletDeltaRecord(transformed.getTargetVersion(), appliedDelta,
                transformedDelta);
        waveletState.appendDelta(deltaRecord);

        return deltaRecord;
    }

    /**
     * @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 void requestHistory(HashedVersion startVersion, HashedVersion endVersion,
            Receiver<ByteStringMessage<ProtocolAppliedWaveletDelta>> receiver)
            throws AccessControlException, WaveletStateException {
        acquireReadLock();
        try {
            checkStateOk();
            checkVersionIsDeltaBoundary(startVersion, "start version");
            checkVersionIsDeltaBoundary(endVersion, "end version");
            waveletState.getAppliedDeltaHistory(startVersion, endVersion, receiver);
        } finally {
            releaseReadLock();
        }
    }

    @Override
    public void requestTransformedHistory(HashedVersion startVersion, HashedVersion endVersion,
            Receiver<TransformedWaveletDelta> receiver) throws AccessControlException, WaveletStateException {
        awaitLoad();
        acquireReadLock();
        try {
            checkStateOk();
            checkVersionIsDeltaBoundary(startVersion, "start version");
            checkVersionIsDeltaBoundary(endVersion, "end version");
            waveletState.getTransformedDeltaHistory(startVersion, endVersion, receiver);
        } 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();
        }
    }

    @Nullable
    protected HashedVersion getCurrentVersion() {
        if (waveletState == null)
            return null;

        return waveletState.getCurrentVersion();
    }

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