/** * 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 org.waveprotocol.wave.common.logging.LoggerBundle; import org.waveprotocol.wave.concurrencycontrol.client.ConcurrencyControl; import org.waveprotocol.wave.concurrencycontrol.client.ServerConnection; import org.waveprotocol.wave.concurrencycontrol.common.ChannelException; import org.waveprotocol.wave.concurrencycontrol.common.Recoverable; import org.waveprotocol.wave.concurrencycontrol.common.ResponseCode; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.TransformException; 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.Preconditions; import org.waveprotocol.wave.model.version.HashedVersion; import java.util.List; /** * This operation channel implementation mediates between a WaveletDeltaChannel * and client ConcurrencyControl. It sends operations via concurrency control in * deltas through a wavelet delta channel, and accepts deltas from the the delta * channel as broadcast from the server. Ops are transformed and queued in the * ConcurrencyControl class. * * The delta channel is connected through a raw wavelet channel. * * ConcurrencyControl * ^ * | * v * OperationChannel <=> WaveletDeltaChannel <=> WaveletChannel * * The channel allows reconnection when after some failures by providing * reconnection versions and reconciling concurrency control state * with the server's state. Recovery can fail when more than one client modifies * the wave, and the wave server crashes losing history about the order of * deltas accepted. * * @see ConcurrencyControl * @see WaveletDeltaChannel * * @author zdwang@google.com (David Wang) * @author anorth@google.com (Alex North) */ class OperationChannelImpl implements InternalOperationChannel { private static enum State { /** Initial state, or when we are recovering from disconnection. */ NOT_CONNECTED, /** After onConnection(). */ CONNECTED, /** After close(). */ CLOSED } private final LoggerBundle logger; private final Accessibility accessibility; // "Final", but null after the channel is closed. private WaveletDeltaChannel deltaChannel; private Listener listener; private ConcurrencyControl cc; private State state = State.NOT_CONNECTED; /** * CC server connection which passes requests to the delta channel. */ private final ServerConnection ccServerConnection = new ServerConnection() { @Override public boolean isOpen() { return state == State.CONNECTED; } @Override public void send(final WaveletDelta delta) { if (logger.trace().shouldLog()) { logger.trace().log("sending delta: ", delta); } deltaChannel.send(new WaveletDeltaChannel.Transmitter() { public ClientMessage takeMessage() { return new ClientMessage(delta, false); } }); } @Override public String debugGetProfilingInfo() { return deltaChannel.debugGetProfilingInfo(); } }; /** * CC client listener which passes events on to the connection listener. */ private final ConcurrencyControl.ConnectionListener ccListener = new ConcurrencyControl.ConnectionListener() { @Override public void onOperationReceived() { signalOperationReceived(); } }; /** * Constructs an operation channel for the named wave. * * Any sent operations before onConnection are queued to be sent upon * connection. * * @param opsLogger logger to use for operationChannel logging * @param deltaChannel delta channel server connection * @param cc concurrency control module */ OperationChannelImpl(LoggerBundle opsLogger, WaveletDeltaChannel deltaChannel, ConcurrencyControl cc, Accessibility accessibility) { this.deltaChannel = deltaChannel; this.cc = cc; this.accessibility = accessibility; this.logger = opsLogger; this.state = State.NOT_CONNECTED; cc.initialise(ccServerConnection, ccListener); } // WaveletDeltaChannel.Receiver implementation // @Override public void onConnection(HashedVersion connectVersion, HashedVersion currentVersion) throws ChannelException { Preconditions.checkState(state == State.NOT_CONNECTED, "OperationChannel received onConnection in state " + state); Preconditions.checkState(cc != null, "Cannot connect a closed channel"); state = State.CONNECTED; cc.onOpen(connectVersion, currentVersion); } @Override public void onDelta(TransformedWaveletDelta delta) throws ChannelException { try { if (logger.trace().shouldLog()) { logger.trace().log("Received delta: ", delta); } cc.onServerDelta(delta); } catch (TransformException e) { throw new ChannelException(ResponseCode.INVALID_OPERATION, "Operation channel failed on server delta: " + this + ", " + deltaChannel + ", " + cc, e, Recoverable.NOT_RECOVERABLE, null, null); } catch (OperationException e) { throw new ChannelException( e.isSchemaViolation() ? ResponseCode.SCHEMA_VIOLATION : ResponseCode.INVALID_OPERATION, "Operation channel failed on server delta: " + this + ", " + deltaChannel + ", " + cc, e, Recoverable.NOT_RECOVERABLE, null, null); } } @Override public void onAck(int opsApplied, HashedVersion signature) throws ChannelException { try { cc.onSuccess(opsApplied, signature); } catch (TransformException e) { throw new ChannelException(ResponseCode.INVALID_OPERATION, "Operation channel failed on ack: " + this + ", " + deltaChannel + ", " + cc, e, Recoverable.NOT_RECOVERABLE, null, null); } } @Override public void onNack(ResponseCode responseCode, String errorString, long version) throws ChannelException { throw new ChannelException(responseCode, "Operation channel failed on nack: code=" + responseCode + ", " + errorString + ", " + this + ", " + deltaChannel + ", " + cc, null, Recoverable.NOT_RECOVERABLE, null, null); } @Override public void onCommit(long version) { cc.onCommit(version); } // BaseOperationChannel implementation // @Override public void setListener(Listener listener) { this.listener = listener; } @Override public void send(WaveletOperation... operations) throws ChannelException { if (state == State.CLOSED) { // TODO(anorth): throw an exception here after fixing clients. logger.error().log("Cannot send to closed operation channel: " + this); } else if (accessibility.isWritable()) { try { cc.onClientOperations(operations); } catch (TransformException e) { throw new ChannelException(ResponseCode.INVALID_OPERATION, "Operation channel failed on send: " + this + ", " + deltaChannel + ", " + cc, e, Recoverable.NOT_RECOVERABLE, null, null); } } else { throw new ChannelException(ResponseCode.NOT_AUTHORIZED, "Attempt to write to inaccessible wavelet", null, Recoverable.NOT_RECOVERABLE, null, null); } } @Override public WaveletOperation receive() { if (state == State.CLOSED) { // TODO(anorth): throw an exception here after Wally doesn't do this. logger.error().log("Cannot receive from closed operation channel: " + this); return null; } else { return cc.receive(); } } @Override public WaveletOperation peek() { if (state == State.CLOSED) { // TODO(anorth): throw an exception here after Wally doesn't do this. logger.error().log("Cannot peek at closed operation channel: " + this); return null; } else { return cc.peek(); } } @Override public List<HashedVersion> getReconnectVersions() { if (state == State.CLOSED) { throw new IllegalStateException("Cannot query closed operation channel: " + this); } return cc.getReconnectionVersions(); } /** * Resets this channel ready to be reconnected. */ @Override public void reset() { state = State.NOT_CONNECTED; } /** * Closes this channel permanently. */ @Override public void close() { Preconditions.checkState(state != State.CLOSED, "Cannot close already-closed channel"); state = State.CLOSED; cc.close(); // Disconnect everything. cc = null; deltaChannel = null; listener = null; } /** * Signals to the listener than an operation is ready. */ private void signalOperationReceived () { if (listener != null) { listener.onOperationReceived(); } } @Override public String toString() { return "Operation Channel State = [state: " + state + "]"; } @Override public String getDebugString() { return cc + "==========\n" + OperationChannelImpl.this.toString() + "==========\n" + deltaChannel; } }