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

Java tutorial

Introduction

Here is the source code for org.waveprotocol.box.server.waveserver.RemoteWaveletContainerImpl.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 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.collect.Maps;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;

import org.apache.commons.codec.binary.Base64;

import org.waveprotocol.box.server.common.CoreWaveletOperationSerializer;
import org.waveprotocol.box.server.waveserver.CertificateManager.SignerInfoPrefetchResultListener;
import org.waveprotocol.wave.crypto.SignatureException;
import org.waveprotocol.wave.crypto.UnknownSignerException;
import org.waveprotocol.wave.federation.FederationErrorProto.FederationError;
import org.waveprotocol.wave.federation.FederationErrors;
import org.waveprotocol.wave.federation.FederationException;
import org.waveprotocol.wave.federation.Proto.ProtocolAppliedWaveletDelta;
import org.waveprotocol.wave.federation.Proto.ProtocolHashedVersion;
import org.waveprotocol.wave.federation.Proto.ProtocolSignature;
import org.waveprotocol.wave.federation.Proto.ProtocolSignedDelta;
import org.waveprotocol.wave.federation.Proto.ProtocolSignerInfo;
import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta;
import org.waveprotocol.wave.federation.WaveletFederationProvider;
import org.waveprotocol.wave.federation.WaveletFederationProvider.HistoryResponseListener;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.wave.WaveletDelta;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.util.logging.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;
import java.util.Map;
import java.util.NavigableMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Remote wavelets differ from local ones in that deltas are not submitted for OT,
 * rather they are updated when a remote wave service provider has applied and sent
 * a delta.
 */
class RemoteWaveletContainerImpl extends WaveletContainerImpl implements RemoteWaveletContainer {
    private static final Log LOG = Log.get(RemoteWaveletContainerImpl.class);

    /**
     * Stores all pending deltas for this wavelet, whose insertions would cause
     * discontiguous blocks of deltas. This must only be accessed under writeLock.
     */
    private final NavigableMap<HashedVersion, ByteStringMessage<ProtocolAppliedWaveletDelta>> pendingDeltas = Maps
            .newTreeMap();

    /**
     * Tracks the highest version commit notice received, which can not be performed
     * due to not yet having the required deltas. This must only be access under writeLock.
     */
    private boolean pendingCommit = false;
    private HashedVersion pendingCommitVersion;

    /**
     * Create a new RemoteWaveletContainerImpl. Just pass through to the parent
     * constructor.
     */
    public RemoteWaveletContainerImpl(WaveletName waveletName, WaveletNotificationSubscriber notifiee,
            ListenableFuture<? extends WaveletState> waveletStateFuture, Executor storageContinuationExecutor) {
        // We pass here null for waveDomain because you have to be explicit
        // participant on remote wavelet to have access permission.
        super(waveletName, notifiee, waveletStateFuture, null, storageContinuationExecutor);
    }

    @Override
    public ListenableFuture<Void> update(final List<ByteString> deltas, final String domain,
            final WaveletFederationProvider federationProvider, final CertificateManager certificateManager) {
        SettableFuture<Void> futureResult = SettableFuture.create();
        internalUpdate(deltas, domain, federationProvider, certificateManager, futureResult);
        return futureResult;
    }

    @Override
    public void commit(HashedVersion version) {
        try {
            awaitLoad();
        } catch (WaveletStateException ex) {
            LOG.warning("Failed to load " + getWaveletName() + " to perform commit.", ex);
            acquireWriteLock();
            markStateCorrupted();
            releaseWriteLock();
            return;
        }

        acquireWriteLock();
        try {
            attemptCommit(version);
        } finally {
            releaseWriteLock();
        }
    }

    /**
     * Attempts to commit at the given version.
     * This will only succeed if we are actually up to date.
     * If not, then the history is assumed to be coming, and so we can just skip the whole task.
     * */
    private void attemptCommit(HashedVersion version) {
        HashedVersion expectedVersion = getCurrentVersion();
        if (expectedVersion == null || version.getVersion() == expectedVersion.getVersion()) {
            LOG.info("Committed " + getWaveletName() + " at version " + version.getVersion());
            persist(version, ImmutableSet.<String>of());
            if (pendingCommitVersion == null || (version.getVersion() >= pendingCommitVersion.getVersion())) {
                pendingCommit = false;
            }
        } else {
            LOG.info("Ignoring commit request at " + version.getVersion() + " since only at "
                    + expectedVersion.getVersion());
            if (pendingCommitVersion == null
                    || (pendingCommitVersion != null && pendingCommitVersion.getVersion() < version.getVersion())) {
                pendingCommitVersion = version;
            }
            LOG.info("pendingCommitVersion is now " + pendingCommitVersion.getVersion());
            pendingCommit = true;
        }
    }

    private void internalUpdate(final List<ByteString> deltas, final String domain,
            final WaveletFederationProvider federationProvider, final CertificateManager certificateManager,
            final SettableFuture<Void> futureResult) {
        // Turn raw serialised ByteStrings in to a more useful representation
        final List<ByteStringMessage<ProtocolAppliedWaveletDelta>> appliedDeltas = Lists.newArrayList();
        for (ByteString delta : deltas) {
            try {
                appliedDeltas.add(ByteStringMessage.parseProtocolAppliedWaveletDelta(delta));
            } catch (InvalidProtocolBufferException e) {
                LOG.info("Invalid applied delta protobuf for incoming " + getWaveletName(), e);
                acquireWriteLock();
                try {
                    markStateCorrupted();
                } finally {
                    releaseWriteLock();
                }
                futureResult.setException(new FederationException(
                        FederationErrors.badRequest("Invalid applied delta protocol buffer")));
                return;
            }
        }
        LOG.info("Got update: " + appliedDeltas);

        // Fetch any signer info that we don't already have and then run internalUpdate
        final AtomicInteger numSignerInfoPrefetched = new AtomicInteger(1); // extra 1 for sentinel
        final Runnable countDown = new Runnable() {
            @Override
            public void run() {
                if (numSignerInfoPrefetched.decrementAndGet() == 0) {
                    internalUpdateAfterSignerInfoRetrieval(appliedDeltas, domain, federationProvider,
                            certificateManager, futureResult);
                }
            }
        };
        SignerInfoPrefetchResultListener prefetchListener = new SignerInfoPrefetchResultListener() {
            @Override
            public void onFailure(FederationError error) {
                LOG.warning("Signer info prefetch failed: " + error);
                countDown.run();
            }

            @Override
            public void onSuccess(ProtocolSignerInfo signerInfo) {
                LOG.info("Signer info prefetch success for " + signerInfo.getDomain());
                countDown.run();
            }
        };
        for (ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta : appliedDeltas) {
            ProtocolSignedDelta toVerify = appliedDelta.getMessage().getSignedOriginalDelta();
            HashedVersion deltaEndVersion;
            try {
                deltaEndVersion = AppliedDeltaUtil.calculateResultingHashedVersion(appliedDelta);
            } catch (InvalidProtocolBufferException e) {
                LOG.warning("Skipping illformed applied delta " + appliedDelta, e);
                continue;
            }
            for (ProtocolSignature sig : toVerify.getSignatureList()) {
                if (certificateManager.retrieveSignerInfo(sig.getSignerId()) == null) {
                    LOG.info("Fetching signer info " + Base64.encodeBase64(sig.getSignerId().toByteArray()));
                    numSignerInfoPrefetched.incrementAndGet();
                    certificateManager.prefetchDeltaSignerInfo(federationProvider, sig.getSignerId(),
                            getWaveletName(), deltaEndVersion, prefetchListener);
                }
            }
        }
        // If we didn't fetch any signer info, run internalUpdate immediately
        countDown.run();
    }

    private void internalUpdateAfterSignerInfoRetrieval(
            List<ByteStringMessage<ProtocolAppliedWaveletDelta>> appliedDeltas, final String domain,
            final WaveletFederationProvider federationProvider, final CertificateManager certificateManager,
            final SettableFuture<Void> futureResult) {

        try {
            awaitLoad();
        } catch (WaveletStateException ex) {
            LOG.warning("Failed to load " + getWaveletName() + " to perform update.", ex);
            acquireWriteLock();
            markStateCorrupted();
            releaseWriteLock();
            return;
        }

        LOG.info("Passed signer info check, now applying all " + appliedDeltas.size() + " deltas");
        acquireWriteLock();
        try {
            checkStateOk(); // TODO(soren): if CORRUPTED, throw away wavelet and start again
            HashedVersion expectedVersion = getCurrentVersion();
            boolean haveRequestedHistory = false;

            // Verify signatures of all deltas
            for (ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta : appliedDeltas) {
                try {
                    certificateManager.verifyDelta(appliedDelta.getMessage().getSignedOriginalDelta());
                } catch (SignatureException e) {
                    LOG.warning("Verification failure for " + domain + " incoming " + getWaveletName(), e);
                    throw new WaveServerException("Verification failure", e);
                } catch (UnknownSignerException e) {
                    LOG.severe("Unknown signer for " + domain + " incoming " + getWaveletName()
                            + ", this is BAD! We were supposed to have prefetched it!", e);
                    throw new WaveServerException("Unknown signer", e);
                }
            }

            // Insert all available deltas into pendingDeltas.
            for (ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta : appliedDeltas) {
                LOG.info("Delta incoming: " + appliedDelta);

                // Log any illformed signed original deltas. TODO: Check if this can be removed.
                try {
                    ProtocolWaveletDelta actualDelta = ProtocolWaveletDelta
                            .parseFrom(appliedDelta.getMessage().getSignedOriginalDelta().getDelta());
                    LOG.info("actual delta: " + actualDelta);
                } catch (InvalidProtocolBufferException e) {
                    e.printStackTrace();
                }

                HashedVersion appliedAt;
                try {
                    appliedAt = AppliedDeltaUtil.getHashedVersionAppliedAt(appliedDelta);
                } catch (InvalidProtocolBufferException e) {
                    markStateCorrupted();
                    throw new WaveServerException(
                            "Authoritative server sent delta with badly formed original wavelet delta", e);
                }

                pendingDeltas.put(appliedAt, appliedDelta);
            }

            // Traverse pendingDeltas while we have any to process.
            ImmutableList.Builder<WaveletDeltaRecord> resultingDeltas = ImmutableList.builder();
            while (pendingDeltas.size() > 0) {
                Map.Entry<HashedVersion, ByteStringMessage<ProtocolAppliedWaveletDelta>> first = pendingDeltas
                        .firstEntry();
                HashedVersion appliedAt = first.getKey();
                ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta = first.getValue();

                if (LOG.isInfoLoggable()) {
                    LOG.info("pendingDeltas.size(): " + Integer.toString(pendingDeltas.size()));
                    LOG.info("current appliedAt: " + appliedAt.getVersion() + " expected: "
                            + expectedVersion.getVersion());
                }

                // If we don't have the right version it implies there is a history we need, so set up a
                // callback to request it and fall out of this update
                if (appliedAt.getVersion() > expectedVersion.getVersion()) {
                    LOG.info("Missing history from " + expectedVersion.getVersion() + "-" + appliedAt.getVersion()
                            + ", requesting from upstream for " + getWaveletName());

                    if (federationProvider != null) {
                        // TODO: only one request history should be pending at any one time?
                        // We should derive a new one whenever the active one is finished,
                        // based on the current state of pendingDeltas.
                        federationProvider.requestHistory(getWaveletName(), domain,
                                CoreWaveletOperationSerializer.serialize(expectedVersion),
                                CoreWaveletOperationSerializer.serialize(appliedAt), -1,
                                new HistoryResponseListener() {
                                    @Override
                                    public void onFailure(FederationError error) {
                                        LOG.severe("Callback failure: " + error);
                                    }

                                    @Override
                                    public void onSuccess(List<ByteString> deltaList,
                                            ProtocolHashedVersion lastCommittedVersion, long versionTruncatedAt) {
                                        LOG.info("Got response callback: " + getWaveletName() + ", lcv "
                                                + lastCommittedVersion + " deltaList length = " + deltaList.size());

                                        // Try updating again with the new history
                                        internalUpdate(deltaList, domain, federationProvider, certificateManager,
                                                futureResult);
                                    }
                                });
                        haveRequestedHistory = true;
                    } else {
                        LOG.severe("History request resulted in non-contiguous deltas!");
                    }
                    break;
                }

                // This delta is at the correct (current) version - apply it.
                if (appliedAt.getVersion() == expectedVersion.getVersion()) {
                    // Confirm that the applied at hash matches the expected hash.
                    if (!appliedAt.equals(expectedVersion)) {
                        markStateCorrupted();
                        throw new WaveServerException("Incoming delta applied at version " + appliedAt.getVersion()
                                + " is not applied to the correct hash");
                    }

                    LOG.info("Applying delta for version " + appliedAt.getVersion());
                    try {
                        WaveletDeltaRecord applicationResult = transformAndApplyRemoteDelta(appliedDelta);
                        long opsApplied = applicationResult.getResultingVersion().getVersion()
                                - expectedVersion.getVersion();
                        if (opsApplied != appliedDelta.getMessage().getOperationsApplied()) {
                            throw new OperationException("Operations applied here do not match the authoritative"
                                    + " server claim (got " + opsApplied + ", expected "
                                    + appliedDelta.getMessage().getOperationsApplied() + ".");
                        }
                        // Add transformed result to return list.
                        resultingDeltas.add(applicationResult);
                        LOG.fine("Applied delta: " + appliedDelta);
                    } catch (OperationException e) {
                        markStateCorrupted();
                        throw new WaveServerException("Couldn't apply authoritative delta", e);
                    } catch (InvalidProtocolBufferException e) {
                        markStateCorrupted();
                        throw new WaveServerException("Couldn't apply authoritative delta", e);
                    } catch (InvalidHashException e) {
                        markStateCorrupted();
                        throw new WaveServerException("Couldn't apply authoritative delta", e);
                    }

                    // TODO: does waveletData update?
                    expectedVersion = getCurrentVersion();
                } else {
                    LOG.warning("Got delta from the past: " + appliedDelta);
                }

                pendingDeltas.remove(appliedAt);
            }

            commitAndNotifyResultingDeltas(resultingDeltas, futureResult);

        } catch (WaveServerException e) {
            LOG.warning("Update failure", e);
            // TODO(soren): make everyone throw FederationException instead
            // of WaveServerException so we don't have to translate between them here
            futureResult.setException(new FederationException(FederationErrors.badRequest(e.getMessage())));
        } finally {
            releaseWriteLock();
        }
    }

    /**
     * Commits the resulting deltas, notifying the server of them.
     * Assumes that everything in resultingDeltas is now in-order, since
     * even if the original stream was non-contiguous, we have requestedHistory.
     * Even if not, it is still safe to commit up to the fragmented point.
     */
    private void commitAndNotifyResultingDeltas(ImmutableList.Builder<WaveletDeltaRecord> resultingDeltas,
            final SettableFuture<Void> futureResult) {
        if (!resultingDeltas.build().isEmpty()) {
            notifyOfDeltas(resultingDeltas.build(), ImmutableSet.<String>of());
            futureResult.set(null);

            //Attempt to run any pending commit
            if (pendingCommit) {
                releaseWriteLock();
                commit(pendingCommitVersion);
                acquireWriteLock();
            }
        } else {
            LOG.info("No deltas in list (fetching history?), ignoring callback");
        }
    }

    /**
     * Apply a serialised applied delta to a remote wavelet. This assumes the
     * caller has validated that the delta is at the correct version and can be
     * applied to the wavelet. Must be called with writelock held.
     *
     * @param appliedDelta that is to be applied to the wavelet in its serialised form
     * @return the transformed and applied delta.
     * @throws AccessControlException if the supplied Delta's historyHash does not
     *         match the canonical history.
     * @throws WaveServerException if the delta transforms away.
     */
    private WaveletDeltaRecord transformAndApplyRemoteDelta(
            ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta) throws OperationException,
            AccessControlException, InvalidHashException, InvalidProtocolBufferException, WaveServerException {
        // The serialised hashed version should actually match the currentVersion at this point, since
        // the caller of transformAndApply delta will have made sure the applied deltas are ordered
        HashedVersion hashedVersion = AppliedDeltaUtil.getHashedVersionAppliedAt(appliedDelta);
        Preconditions.checkState(hashedVersion.equals(getCurrentVersion()),
                "Applied delta must apply to current version");

        // Extract the serialised wavelet delta
        ByteStringMessage<ProtocolWaveletDelta> protocolDelta = ByteStringMessage
                .parseProtocolWaveletDelta(appliedDelta.getMessage().getSignedOriginalDelta().getDelta());
        WaveletDelta delta = CoreWaveletOperationSerializer.deserialize(protocolDelta.getMessage());

        // Transform operations against earlier deltas, if necessary
        WaveletDelta transformed = maybeTransformSubmittedDelta(delta);
        if (transformed.getTargetVersion().equals(delta.getTargetVersion())) {
            // No transformation took place.
            // As a sanity check, the hash from the applied delta should NOT be set (an optimisation, but
            // part of the protocol).
            if (appliedDelta.getMessage().hasHashedVersionAppliedAt()) {
                LOG.warning("Hashes are the same but applied delta has hashed_version_applied_at");
                // TODO: re-enable this exception for version 0.3 of the spec
                //        throw new InvalidHashException("Applied delta and its contained delta have same hash");
            }
        }

        if (transformed.size() == 0) {
            // The host shouldn't be forwarding empty deltas!
            markStateCorrupted();
            throw new WaveServerException("Couldn't apply authoritative delta, " + "it transformed away at version "
                    + transformed.getTargetVersion().getVersion());
        }

        if (!transformed.getTargetVersion().equals(hashedVersion)) {
            markStateCorrupted();
            throw new WaveServerException(
                    "Couldn't apply authoritative delta, " + "it transformed to wrong version. Expected "
                            + hashedVersion + ", actual " + transformed.getTargetVersion().getVersion());
        }

        // Apply the delta to the local wavelet state.
        // This shouldn't fail since the delta is from the authoritative server, so if it fails
        // then the wavelet is corrupted (and the caller of this method will sort it out).
        return applyDelta(appliedDelta, transformed);
    }
}