/** * Copyright 2008 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.wave.concurrencycontrol.channel; import static org.waveprotocol.wave.model.wave.Constants.NO_VERSION; import org.waveprotocol.wave.common.logging.LoggerBundle; import org.waveprotocol.wave.concurrencycontrol.client.ConcurrencyControl; import org.waveprotocol.wave.concurrencycontrol.common.ChannelException; import org.waveprotocol.wave.concurrencycontrol.common.CorruptionDetail; import org.waveprotocol.wave.concurrencycontrol.common.Recoverable; import org.waveprotocol.wave.concurrencycontrol.common.ResponseCode; import org.waveprotocol.wave.concurrencycontrol.common.UnsavedDataListenerFactory; import org.waveprotocol.wave.model.id.IdFilter; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.id.WaveletName; 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.util.CollectionUtils; import org.waveprotocol.wave.model.util.FuzzingBackOffScheduler; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.util.Scheduler; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.version.HashedVersionFactory; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; import org.waveprotocol.wave.model.wave.data.impl.EmptyWaveletSnapshot; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * Multiplexes several {@link OperationChannel operation channels} over one * {@link ViewChannel view channel}. * * * |- OperationChannelMultiplexer -----------------------------------------| * | | * | |-Stacklet---------------------------------| | * | | OperationChannel <-> WaveletDeltaChannel |-| | * <-> | |------------------------------------------| |-| <=> View Channel | <-> WaveService * | |------------------------------------------| | | * | |------------------------------------------| | * | | * | All exceptions are directed here | * |-----------------------------------------------------------------------| * * Note: * * All exceptions that are emitted from using the OperationChannel or * OperationChannelMultiplexer interfaces are caught in this class. * i.e. when the client calls methods from the left part of the diagram. * * All exceptions generated as a result of handling server messages in ViewChannel * are routed here through onException(). i.e. when the WaveService calls methods on * the right part of the diagram through call backs. * * This class is responsible for reporting all the exceptions to the user. * */ public class OperationChannelMultiplexerImpl implements OperationChannelMultiplexer { /** * Binds together both ends of a delta channel. */ interface MultiplexedDeltaChannel extends WaveletDeltaChannel, WaveletChannel.Listener { } /** * Factory for creating delta channels. */ interface DeltaChannelFactory { /** * Creates a delta channel. * * @param waveletChannel channel through which the delta channel * communicates */ MultiplexedDeltaChannel create(WaveletChannel waveletChannel); } /** * Factory for operation channels. */ interface OperationChannelFactory { /** * Creates an operation channel. * * @param deltaChannel channel through which the op channel communicates * @param waveletId wavelet id for the new operation channel * @param startVersion the version to start from * @param accessibility accessibility of the new channel * @return a new operation channel. */ InternalOperationChannel create(WaveletDeltaChannel deltaChannel, WaveletId waveletId, HashedVersion startVersion, Accessibility accessibility); } /** * A per-wavelet stack above this multiplexer. A stacklet forwards message * from the server to a listener at the bottom of the stacklet (a delta * channel). When communications fail a stacklet fetches reconnection version * from the contained operation channel. */ private static class Stacklet implements WaveletChannel.Listener { private final MultiplexedDeltaChannel deltaChannel; private final InternalOperationChannel opChannel; private boolean firstMessageReceived; private boolean dropAdditionalSnapshot; /** * Creates a stacklet. * * @param deltaChannel delta channel at the bottom of the stacklet * @param opChannel operation channel at the top of the stacklet * @param dropSnapshot whether to expect and drop an additional snapshot * after the first message. */ private Stacklet(MultiplexedDeltaChannel deltaChannel, InternalOperationChannel opChannel, boolean dropSnapshot) { this.deltaChannel = deltaChannel; this.opChannel = opChannel; this.firstMessageReceived = false; this.dropAdditionalSnapshot = dropSnapshot; } public void onWaveletSnapshot(ObservableWaveletData wavelet, HashedVersion lastCommittedVersion, HashedVersion currentVersion) throws ChannelException { // When a channel is created locally we fake an initial empty // snapshot. The server still sends one when it creates the wavelet // though, so it's dropped it here if that's expected. // See createOperationChannel(). if (!firstMessageReceived) { firstMessageReceived = true; } else if (dropAdditionalSnapshot) { // TODO(anorth): check the snapshot is as expected, even though // it's dropped. dropAdditionalSnapshot = false; return; } deltaChannel.onWaveletSnapshot(wavelet, lastCommittedVersion, currentVersion); } @Override public void onWaveletUpdate(List<TransformedWaveletDelta> deltas, HashedVersion lastCommittedVersion, HashedVersion currentVersion) throws ChannelException { if (!firstMessageReceived) { firstMessageReceived = true; } deltaChannel.onWaveletUpdate(deltas, lastCommittedVersion, currentVersion); } /** * Resets this stacklet ready for reconnection. */ public void reset() { deltaChannel.reset(opChannel); opChannel.reset(); } /** * Closes this stacklet permanently. */ public void close() { deltaChannel.reset(null); opChannel.close(); } public OperationChannel getOperationChannel() { return opChannel; } public boolean isExpectingSnapshot() { return dropAdditionalSnapshot; } } /** * Holder class for the copious number of loggers. */ public static class LoggerContext { public final LoggerBundle ops; public final LoggerBundle delta; public final LoggerBundle cc; public final LoggerBundle view; public LoggerContext(LoggerBundle ops, LoggerBundle delta, LoggerBundle cc, LoggerBundle view) { this.ops = ops; this.delta = delta; this.cc = cc; this.view = view; } } /** Multiplexer state. */ private static enum State { NOT_CONNECTED, CONNECTED, RECONNECTING } /** Wave id for channels in this mux. */ private final WaveId waveId; /** Multiplexed channels, indexed by wavelet id. */ private final Map<WaveletId, Stacklet> channels = CollectionUtils.newHashMap(); /** Factory for creating delta channels. */ private final DeltaChannelFactory deltaChannelFactory; /** Factory for creating operation-channel stacks on top of wave services. */ private final OperationChannelFactory opChannelFactory; /** Factory for creating a view channel */ private final ViewChannelFactory viewFactory; /** Logger. */ private final LoggerBundle logger; /** A stateful manager/factory for unsaved data listeners */ private final UnsavedDataListenerFactory unsavedDataListenerFactory; /** Synthesizer of initial wavelet snapshots for locally-created wavelets. */ private final ObservableWaveletData.Factory<?> dataFactory; /** Produces hashed versions. */ private final HashedVersionFactory hashFactory; /** List of commands to run when the underlying view becomes connected. */ private final List<Runnable> onConnected = CollectionUtils.newArrayList(); // // Mutable state. // /** Connection state of the mux. */ private State state; /** Whether the initial open of the mux has finished. */ private boolean openFinished = false; /** * Underlying multiplexed view channel; created on reconnection, set null on * close. */ private ViewChannel viewChannel; /** * Tag identifying which view connection is current. Changes on each * reconnection. */ private int connectionTag = 0; /** Filter specifying wavelets to open. */ private IdFilter waveletFilter; /** Listener for handling new operation channels. */ private Listener muxListener; /** Used to backoff when reconnecting. */ private final Scheduler scheduler; /** * Creates factory for building delta channels. * * @param logger logger to use for created channels */ private static DeltaChannelFactory createDeltaChannelFactory(final LoggerBundle logger) { return new DeltaChannelFactory() { @Override public MultiplexedDeltaChannel create(WaveletChannel waveletChannel) { return new WaveletDeltaChannelImpl(waveletChannel, logger); } }; } /** * Creates a factory for building operation channels on a wave. * * @param waveId wave id * @param unsavedDataListenerFactory factory for unsaved data listeners * @param loggers logger bundle * @return a new operation channel factory */ private static OperationChannelFactory createOperationChannelFactory(final WaveId waveId, final UnsavedDataListenerFactory unsavedDataListenerFactory, final LoggerContext loggers) { return new OperationChannelFactory() { @Override public InternalOperationChannel create(WaveletDeltaChannel deltaChannel, WaveletId waveletId, HashedVersion startVersion, Accessibility accessibility) { ConcurrencyControl cc = new ConcurrencyControl(loggers.cc, startVersion); if (unsavedDataListenerFactory != null) { cc.setUnsavedDataListener(unsavedDataListenerFactory.create(waveletId)); } return new OperationChannelImpl(loggers.ops, deltaChannel, cc, accessibility); } }; } /** * Creates a multiplexer. * * WARNING: the scheduler should provide back-off. Providing a scheduler which * executes immediately or does not back off may cause denial-of-service-like * reconnection attempts against the servers. Use something like * {@link FuzzingBackOffScheduler}. * * @param waveId wave id to open * @param viewFactory factory for opening view channels * @param dataFactory factory for making snapshots of empty wavelets * @param loggers log targets * @param unsavedDataListenerFactory a factory for adding listeners * @param scheduler scheduler for reconnection * @param hashFactory factory for hashed versions */ public OperationChannelMultiplexerImpl(WaveId waveId, ViewChannelFactory viewFactory, ObservableWaveletData.Factory<?> dataFactory, LoggerContext loggers, UnsavedDataListenerFactory unsavedDataListenerFactory, Scheduler scheduler, HashedVersionFactory hashFactory) { // Construct default dependency implementations, based on given arguments. this(waveId, createDeltaChannelFactory(loggers.delta), createOperationChannelFactory(waveId, unsavedDataListenerFactory, loggers), viewFactory, dataFactory, scheduler, loggers.view, unsavedDataListenerFactory, hashFactory); Preconditions.checkNotNull(dataFactory, "null dataFactory"); } /** * Creates a multiplexer (direct dependency arguments only). Exposed as * package-private for testing. * * @param opChannelFactory factory for creating operation-channel stacks * @param channelFactory factory for creating the underlying view channel * @param dataFactory factory for creating wavelet snapshots * @param scheduler used to back off when reconnecting. assumed not null. * @param logger log target * @param unsavedDataListenerFactory * @param hashFactory factory for hashed versions */ OperationChannelMultiplexerImpl( WaveId waveId, DeltaChannelFactory deltaChannelFactory, OperationChannelFactory opChannelFactory, ViewChannelFactory channelFactory, ObservableWaveletData.Factory<?> dataFactory, Scheduler scheduler, LoggerBundle logger, UnsavedDataListenerFactory unsavedDataListenerFactory, HashedVersionFactory hashFactory) { this.waveId = waveId; this.deltaChannelFactory = deltaChannelFactory; this.opChannelFactory = opChannelFactory; this.viewFactory = channelFactory; this.dataFactory = dataFactory; this.logger = logger; this.unsavedDataListenerFactory = unsavedDataListenerFactory; this.state = State.NOT_CONNECTED; this.scheduler = scheduler; this.hashFactory = hashFactory; } @Override public void open(Listener listener, IdFilter waveletFilter, Collection<KnownWavelet> knownWavelets) { this.muxListener = listener; this.waveletFilter = waveletFilter; try { if (!knownWavelets.isEmpty()) { for (KnownWavelet knownWavelet : knownWavelets) { Preconditions.checkNotNull(knownWavelet.snapshot, "Snapshot has no wavelet"); Preconditions.checkNotNull(knownWavelet.committedVersion, "Known wavelet has null committed version"); boolean dropAdditionalSnapshot = false; addOperationChannel(knownWavelet.snapshot.getWaveletId(), knownWavelet.snapshot, knownWavelet.committedVersion, knownWavelet.accessibility, dropAdditionalSnapshot); } // consider the wave as if open has finished. maybeOpenFinished(); } Map<WaveletId, List<HashedVersion>> knownSignatures = signaturesFromWavelets(knownWavelets); connect(knownSignatures); } catch (ChannelException e) { shutdown("Multiplexer open failed.", e); } } @Override public void open(Listener listener, IdFilter waveletFilter) { open(listener, waveletFilter, Collections.<KnownWavelet>emptyList()); } @Override public void close() { shutdown(ResponseCode.OK, "View closed.", null); } @Override public void createOperationChannel(WaveletId waveletId, ParticipantId creator) { if (channels.containsKey(waveletId)) { Preconditions.illegalArgument("Operation channel already exists for: " + waveletId); } // Create the new channel, and fake an initial snapshot. // TODO(anorth): inject a clock for providing timestamps. HashedVersion v0 = hashFactory.createVersionZero(WaveletName.of(waveId, waveletId)); final ObservableWaveletData emptySnapshot = dataFactory.create( new EmptyWaveletSnapshot(waveId, waveletId, creator, v0, System.currentTimeMillis())); try { boolean dropAdditionalSnapshot = true; addOperationChannel(waveletId, emptySnapshot, v0, Accessibility.READ_WRITE, dropAdditionalSnapshot); } catch (ChannelException e) { shutdown("Creating operation channel failed.", e); } } /** * Creates a view channel listener. The listener will forward messages to * stacklets while {@link #connectionTag} has the value it had at creation * time. When a channel (re)connects the tag changes. * * @param expectedWavelets wavelets and reconnection versions we expect to * receive a message for before * {@link ViewChannel.Listener#onOpenFinished()} */ private ViewChannel.Listener createViewListener( final Map<WaveletId, List<HashedVersion>> expectedWavelets) { final int expectedTag = connectionTag; return new ViewChannel.Listener() { /** * Wavelets for which we have not yet seen a message, or null after * onOpenFinished. */ Set<WaveletId> missingWavelets = CollectionUtils.newHashSet(expectedWavelets.keySet()); @Override public void onSnapshot(WaveletId waveletId, ObservableWaveletData wavelet, HashedVersion lastCommittedVersion, HashedVersion currentVersion) throws ChannelException { if (connectionTag == expectedTag) { removeMissingWavelet(waveletId); try { // Forward message to the appropriate stacklet, creating it if // needed. Stacklet stacklet = channels.get(waveletId); boolean dropAdditionalSnapshot = false; // TODO(anorth): Do better than guessing at accessibility here. if (stacklet == null) { createStacklet(waveletId, wavelet, Accessibility.READ_WRITE, dropAdditionalSnapshot); stacklet = channels.get(waveletId); } else if (!stacklet.isExpectingSnapshot()) { // Replace the existing stacklet by first removing the wavelet // and then adding the newly connected one. channels.remove(waveletId); unsavedDataListenerFactory.destroy(waveletId); muxListener.onOperationChannelRemoved(stacklet.getOperationChannel(), waveletId); createStacklet(waveletId, wavelet, Accessibility.READ_WRITE, dropAdditionalSnapshot); stacklet = channels.get(waveletId); } stacklet.onWaveletSnapshot(wavelet, lastCommittedVersion, currentVersion); } catch (ChannelException e) { throw exceptionWithContext(e, waveletId); } } } @Override public void onUpdate(WaveletId waveletId, List<TransformedWaveletDelta> deltas, HashedVersion lastCommittedVersion, HashedVersion currentVersion) throws ChannelException { if (connectionTag == expectedTag) { removeMissingWavelet(waveletId); maybeResetScheduler(deltas); try { Stacklet stacklet = channels.get(waveletId); if (stacklet == null) { //TODO(user): Figure out the right exception to throw here. throw new IllegalStateException("Received deltas with no stacklet present!"); } stacklet.onWaveletUpdate(deltas, lastCommittedVersion, currentVersion); } catch (ChannelException e) { throw exceptionWithContext(e, waveletId); } } else { logger.trace().log("Mux dropping update from defunct view"); } } @Override public void onOpenFinished() throws ChannelException { if (connectionTag == expectedTag) { if (missingWavelets == null) { // TODO(anorth): Add an error code for a protocol error and use // it here. throw new ChannelException(ResponseCode.INTERNAL_ERROR, "Multiplexer received openFinished twice", null, Recoverable.NOT_RECOVERABLE, waveId, null); } // If a missing wavelet could be reconnected at version zero then // fake the resync message here. The server no longer knows about // the wavelet so we should resubmit changes from version zero. Iterator<WaveletId> itr = missingWavelets.iterator(); while (itr.hasNext()) { WaveletId maybeMissing = itr.next(); List<HashedVersion> resyncVersions = expectedWavelets.get(maybeMissing); Preconditions.checkState(!resyncVersions.isEmpty(), "Empty resync versions for wavelet " + maybeMissing); if (resyncVersions.get(0).getVersion() == 0) { Stacklet stacklet = channels.get(maybeMissing); if (stacklet == null) { Preconditions.illegalState("Resync wavelet has no stacklet. Channels: " + channels.keySet() + ", resync: " + expectedWavelets.keySet()); } WaveletName wavelet = WaveletName.of(waveId, maybeMissing); List<TransformedWaveletDelta> resyncDeltaList = createVersionZeroResync(wavelet); HashedVersion v0 = hashFactory.createVersionZero(wavelet); stacklet.onWaveletUpdate(resyncDeltaList, v0, v0); itr.remove(); } } // Check we received a message for each expected wavelet. if (!missingWavelets.isEmpty()) { throw new ChannelException(ResponseCode.NOT_AUTHORIZED, "Server didn't acknowledge known wavelets; perhaps access has been lost: " + missingWavelets, null, Recoverable.NOT_RECOVERABLE, waveId, null); } missingWavelets = null; maybeOpenFinished(); } else { logger.trace().log("Mux dropping openFinished from defunct view"); } } @Override public void onConnected() { if (connectionTag == expectedTag) { OperationChannelMultiplexerImpl.this.onConnected(); } else { logger.trace().log("Mux dropping onConnected from defunct view"); } } @Override public void onClosed() { if (connectionTag == expectedTag) { reconnect(null); } else { logger.trace().log("Mux dropping onClosed from defunct view"); } } @Override public void onException(ChannelException e) { if (connectionTag == expectedTag) { onChannelException(e); } else { logger.trace().log("Mux dropping failure from defunct view"); } } /** * Adds a wavelet id to the set of seen ids if they are being tracked. */ private void removeMissingWavelet(WaveletId id) { if (missingWavelets != null) { missingWavelets.remove(id); } } /** * Resets the reconnection scheduler if a message indicates * the connection is somewhat ok. */ private void maybeResetScheduler(List<TransformedWaveletDelta> deltas) { // The connection is probably ok if we receive a delta. A snapshot // is not sufficient since some are locally generated. The delta need // not have ops; a reconnection delta is enough. if ((deltas.size() > 0)) { scheduler.reset(); } } }; } /** * Creates a stacklet and (optionally) initialises it with a snapshot. * * @param waveletId the wavelet id of the channel to create * @param snapshot the wavelet container for the new channel * @param committedVersion the committed version for the new channel * @param accessibility accessibility the user currently has to the wavelet * @param initialiseLocalChannel whether to send the snapshot through the * stacklet, in which case it should expect and drop an additional * snapshot from the network */ private void addOperationChannel(final WaveletId waveletId, ObservableWaveletData snapshot, HashedVersion committedVersion, Accessibility accessibility, boolean initialiseLocalChannel) throws ChannelException { final Stacklet stacklet = createStacklet(waveletId, snapshot, accessibility, initialiseLocalChannel); if (initialiseLocalChannel) { final HashedVersion currentVersion = snapshot.getHashedVersion(); initialiseLocallyCreatedStacklet(stacklet, waveletId, snapshot, committedVersion, currentVersion); } } /** * This is an ugly work-around the lack of ability to add channels to a view * in the view service API. We need to send some message through the stacklet * so it's connected but the server can't send us any message until we submit * the first delta, which requires a connected stacklet... */ private void initialiseLocallyCreatedStacklet(final Stacklet stacklet, final WaveletId waveletId, final ObservableWaveletData snapshot, final HashedVersion committedVersion, final HashedVersion currentVersion) throws ChannelException { if (state == State.CONNECTED) { try { stacklet.onWaveletSnapshot(snapshot, committedVersion, currentVersion); } catch (ChannelException e) { throw exceptionWithContext(e, waveletId); } } else { // Delay connecting the stacklet until the underlying view is connected. onConnected.add(new Runnable() { public void run() { try { stacklet.onWaveletSnapshot(snapshot, committedVersion, currentVersion); } catch (ChannelException e) { shutdown("Fake snapshot for wavelet channel " + waveId + "/" + waveletId + "failed", exceptionWithContext(e, waveletId)); } } }); } } /** * Adds a new operation-channel stacklet to this multiplexer and notifies the * listener of the new channel's creation. * * @param waveletId id of the concurrency domain for the new channel * @param snapshot wavelet initial state snapshot * @param accessibility accessibility of the stacklet; if not * {@link Accessibility#READ_WRITE} then * the stacklet will fail on send * @param dropSnapshot whether to expect and drop an additional snapshot from * the view */ private Stacklet createStacklet(final WaveletId waveletId, ObservableWaveletData snapshot, Accessibility accessibility, boolean dropSnapshot) { if (channels.containsKey(waveletId)) { Preconditions.illegalArgument("Cannot create duplicate channel for wavelet: " + waveId + "/" + waveletId); } WaveletChannel waveletChannel = createWaveletChannel(waveletId); MultiplexedDeltaChannel deltaChannel = deltaChannelFactory.create(waveletChannel); InternalOperationChannel opChannel = opChannelFactory.create(deltaChannel, waveletId, snapshot.getHashedVersion(), accessibility); Stacklet stacklet = new Stacklet(deltaChannel, opChannel, dropSnapshot); stacklet.reset(); channels.put(waveletId, stacklet); if (muxListener != null) { muxListener.onOperationChannelCreated(stacklet.getOperationChannel(), snapshot, accessibility); } return stacklet; } /** * Executes any pending commands in the {@link #onConnected} queue. */ private void onConnected() { state = State.CONNECTED; // Connect all channels created before now. for (Runnable command : onConnected) { command.run(); } onConnected.clear(); } /** * Handles failure of the view channel or an operation channel. * * @param e The exception that caused the channel to fail. */ private void onChannelException(ChannelException e) { if (e.getRecoverable() != Recoverable.RECOVERABLE) { shutdown(e.getResponseCode(), "Channel Exception", e); } else { reconnect(e); } } private void connect(Map<WaveletId, List<HashedVersion>> knownWavelets) { Preconditions.checkState(state != State.CONNECTED, "Cannot connect already-connected channel"); checkConnectVersions(knownWavelets); logger.trace().log("Multiplexer reconnecting wave " + waveId); viewChannel = viewFactory.create(waveId); viewChannel.open(createViewListener(knownWavelets), waveletFilter, knownWavelets); } /** * Checks that reconnect versions are strictly increasing and removes any * that are not accepted by the connection's wavelet filter. */ private void checkConnectVersions(Map<WaveletId, List<HashedVersion>> knownWavelets) { Iterator<Map.Entry<WaveletId, List<HashedVersion>>> itr = knownWavelets.entrySet().iterator(); while (itr.hasNext()) { Map.Entry<WaveletId, List<HashedVersion>> entry = itr.next(); WaveletId id = entry.getKey(); if (IdFilter.accepts(waveletFilter, id)) { long prevVersion = NO_VERSION; for (HashedVersion v : entry.getValue()) { if ((prevVersion != NO_VERSION) && (v.getVersion() <= prevVersion)) { throw new IllegalArgumentException("Invalid reconnect versions for " + waveId + id + ": " + entry.getValue()); } prevVersion = v.getVersion(); } } else { // TODO(anorth): throw an IllegalArgumentException here after fixing // all callers to avoid this. logger.error().log( "Mux for " + waveId + " dropping resync versions for filtered wavelet " + id + ", filter " + waveletFilter); itr.remove(); } } } /** * Terminates all stacklets then reconnects with the known versions * provided by them. * @param exception The exception that caused the reconnection */ private void reconnect(ChannelException exception) { logger.trace().logLazyObjects("Multiplexer disconnected in state ", state , ", reconnecting."); state = State.RECONNECTING; // NOTE(zdwang): don't clear this as we'll lose wavelets if we've never // been connected. This is a reminder. // onConnected.clear(); // Reset each stacklet, collecting the reconnect versions. final Map<WaveletId, List<HashedVersion>> knownWavelets = CollectionUtils.newHashMap(); for (final WaveletId wavelet : channels.keySet()) { final Stacklet stacklet = channels.get(wavelet); stacklet.reset(); knownWavelets.put(wavelet, stacklet.getOperationChannel().getReconnectVersions()); } // Close the view channel and ignore future messages from it. connectionTag++; viewChannel.close(); // Run the connect part in the scheduler scheduler.schedule(new Scheduler.Command() { int tag = connectionTag; @Override public void execute() { if (tag == connectionTag) { // Reconnect by creating another view channel. connect(knownWavelets); } } }); } /** * Shuts down this multiplexer permanently. * * @param reasonCode code representing failure reason. If the value is not * {@code ResponseCode.OK} then the listener will be notified of connection failure. * @param description reason for failure * @param exception any exception that caused the shutdown. */ private void shutdown(ResponseCode reasonCode, String description, Throwable exception) { if (description == null) { description = "(No error description provided)"; } boolean notifyFailure = (reasonCode != ResponseCode.OK); // We are telling the user through UI that the wave is corrupt, so we must also report it // to the server. if (notifyFailure) { if (exception == null) { logger.error().log(description); } else { logger.error().log(description, exception); } } if (viewChannel != null) { // Ignore future messages. connectionTag++; state = State.NOT_CONNECTED; for (Stacklet stacklet : channels.values()) { stacklet.close(); } channels.clear(); viewChannel.close(); viewChannel = null; if (muxListener != null && notifyFailure) { muxListener.onFailed(new CorruptionDetail(reasonCode, description, exception)); } muxListener = null; } } /** * Shuts down this multiplexer permanently after an exception. */ private void shutdown(String message, ChannelException e) { shutdown(e.getResponseCode(), message, e); } /** * Creates a wavelet channel for submissions against a wavelet. * * @param waveletId wavelet id for the channel */ private WaveletChannel createWaveletChannel(final WaveletId waveletId) { return new WaveletChannel() { @Override public void submit(WaveletDelta delta, final SubmitCallback callback) { viewChannel.submitDelta(waveletId, delta, callback); } @Override public String debugGetProfilingInfo() { return viewChannel.debugGetProfilingInfo(waveletId); } }; } private void maybeOpenFinished() { // Forward message to the mux's open listener. if (!openFinished) { openFinished = true; muxListener.onOpenFinished(); } } /** * Wraps a channel exception in another providing wave and wavelet id context. */ private ChannelException exceptionWithContext(ChannelException e, WaveletId waveletId) { return new ChannelException(e.getResponseCode(), "Nested ChannelException", e, e.getRecoverable(), waveId, waveletId); } /** * Constructs a maps of list of wavelet signatures from a collection of * wavelet snapshots. * * Package-private for testing. */ static Map<WaveletId, List<HashedVersion>> signaturesFromWavelets( Collection<KnownWavelet> knownWavelets) { Map<WaveletId, List<HashedVersion>> signatures = new HashMap<WaveletId, List<HashedVersion>>(); for (KnownWavelet knownWavelet : knownWavelets) { if (knownWavelet.accessibility.isReadable()) { ObservableWaveletData snapshot = knownWavelet.snapshot; WaveletId waveletId = snapshot.getWaveletId(); List<HashedVersion> sigs = Collections.singletonList(snapshot.getHashedVersion()); signatures.put(waveletId, sigs); } } return signatures; } /** * Creates a container message mimicking a resync message for a wavelet at * version zero. */ private List<TransformedWaveletDelta> createVersionZeroResync(WaveletName wavelet) { return Collections.singletonList(new TransformedWaveletDelta((ParticipantId) null, hashFactory.createVersionZero(wavelet), 0L, Collections.<WaveletOperation> emptyList())); } }