Package org.waveprotocol.box.server.waveserver

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

/**
* 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 static java.lang.String.format;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;

import org.waveprotocol.box.common.ListReceiver;
import org.waveprotocol.box.common.Receiver;
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.IdURIEncoderDecoder;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.version.HashedVersionFactory;
import org.waveprotocol.wave.model.version.HashedVersionFactoryImpl;
import org.waveprotocol.wave.model.wave.data.ReadableWaveletData;
import org.waveprotocol.wave.model.wave.data.WaveletData;
import org.waveprotocol.wave.util.escapers.jvm.JavaUrlCodec;
import org.waveprotocol.wave.util.logging.Log;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;

/**
* Simplistic {@link DeltaStore}-backed wavelet state implementation
* which goes to persistent storage for every history request.
*
* TODO(soren): rewire this class to be backed by {@link WaveletStore} and
* read the snapshot from there instead of computing it in the
* DeltaStoreBasedWaveletState constructor.
*
* @author soren@google.com (Soren Lassen)
* @author akaplanov@gmail.com (Andew Kaplanov)
*/
class DeltaStoreBasedWaveletState implements WaveletState {

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

  private static final IdURIEncoderDecoder URI_CODEC =
      new IdURIEncoderDecoder(new JavaUrlCodec());

  private static final HashedVersionFactory HASH_FACTORY =
      new HashedVersionFactoryImpl(URI_CODEC);

  private static final Function<WaveletDeltaRecord, TransformedWaveletDelta> TRANSFORMED =
      new Function<WaveletDeltaRecord, TransformedWaveletDelta>() {
        @Override
        public TransformedWaveletDelta apply(WaveletDeltaRecord record) {
          return record.getTransformedDelta();
        }
      };

  /**
   * @return An entry keyed by a hashed version with the given version number,
   *         if any, otherwise null.
   */
  private static <T> Map.Entry<HashedVersion, T> lookupCached(NavigableMap<HashedVersion, T> map,
      long version) {
    // Smallest key with version number >= version.
    HashedVersion key = HashedVersion.unsigned(version);
    Map.Entry<HashedVersion, T> entry = map.ceilingEntry(key);
    return (entry != null && entry.getKey().getVersion() == version) ? entry : null;
  }

  /**
   * Creates a new delta store based state.
   *
   * The executor must ensure that only one thread executes at any time for each
   * state instance.
   *
   * @param deltasAccess delta store accessor
   * @param persistExecutor executor for making persistence calls
   * @return a state initialized from the deltas
   * @throws PersistenceException if a failure occurs while reading or
   *         processing stored deltas
   */
  public static DeltaStoreBasedWaveletState create(DeltaStore.DeltasAccess deltasAccess,
      Executor persistExecutor) throws PersistenceException {
    if (deltasAccess.isEmpty()) {
      return new DeltaStoreBasedWaveletState(deltasAccess, ImmutableList.<WaveletDeltaRecord>of(),
          null, persistExecutor);
    } else {
      try {
        ImmutableList<WaveletDeltaRecord> deltas = readAll(deltasAccess, null);
        WaveletData snapshot = WaveletDataUtil.buildWaveletFromDeltas(deltasAccess.getWaveletName(),
            Iterators.transform(deltas.iterator(), TRANSFORMED));
        return new DeltaStoreBasedWaveletState(deltasAccess, deltas, snapshot, persistExecutor);
      } catch (IOException e) {
        throw new PersistenceException("Failed to read stored deltas", e);
      } catch (OperationException e) {
        throw new PersistenceException("Failed to compose stored deltas", e);
      }
    }
  }

  /**
   * Reads all deltas from persistent storage.
   */
  private static ImmutableList<WaveletDeltaRecord> readAll(WaveletDeltaRecordReader reader,
      ConcurrentNavigableMap<HashedVersion, WaveletDeltaRecord> cachedDeltas)
      throws IOException {
    HashedVersion startVersion = HASH_FACTORY.createVersionZero(reader.getWaveletName());
    HashedVersion endVersion = reader.getEndVersion();
    ListReceiver<WaveletDeltaRecord> receiver = new ListReceiver<WaveletDeltaRecord>();
    readDeltasInRange(reader, cachedDeltas, startVersion, endVersion, receiver);
    return ImmutableList.copyOf(receiver);
  }

  private static void readDeltasInRange(WaveletDeltaRecordReader reader,
      ConcurrentNavigableMap<HashedVersion, WaveletDeltaRecord> cachedDeltas,
      HashedVersion startVersion, HashedVersion endVersion, Receiver<WaveletDeltaRecord> receiver)
      throws IOException {
    WaveletDeltaRecord delta = getDelta(reader, cachedDeltas, startVersion);
    Preconditions.checkArgument(delta != null && delta.getAppliedAtVersion().equals(startVersion),
        "invalid start version");
    for (;;) {
      if (!receiver.put(delta)) {
        return;
      }
      if (delta.getResultingVersion().getVersion() >= endVersion.getVersion()) {
        break;
      }
      delta = getDelta(reader, cachedDeltas, delta.getResultingVersion());
      if (delta == null) {
        break;
      }
    }
    Preconditions.checkArgument(delta != null && delta.getResultingVersion().equals(endVersion),
        "invalid end version");
  }

  private static WaveletDeltaRecord getDelta(WaveletDeltaRecordReader reader,
      ConcurrentNavigableMap<HashedVersion, WaveletDeltaRecord> cachedDeltas,
      HashedVersion version) throws IOException {
    WaveletDeltaRecord delta = reader.getDelta(version.getVersion());
    if (delta == null && cachedDeltas != null) {
      delta = cachedDeltas.get(version);
    }
    return delta;
  }

  private final Executor persistExecutor;
  private final HashedVersion versionZero;
  private final DeltaStore.DeltasAccess deltasAccess;

  /** The lock that guards access to persistence related state. */
  private final Object persistLock = new Object();

  /**
   * Indicates the version of the latest appended delta that was already requested to be
   * persisted.
   */
  private HashedVersion latestVersionToPersist = null;

  /** The persist task that will be executed next. */
  private ListenableFutureTask<Void> nextPersistTask = null;

  /**
   * Processes the persist task and checks if there is another task to do when
   * one task is done. In such a case, it writes all waiting to be persisted
   * deltas to persistent storage in one operation.
   */
  private final Callable<Void> persisterTask = new Callable<Void>() {
    @Override
    public Void call() throws PersistenceException {
      HashedVersion last;
      HashedVersion version;
      synchronized (persistLock) {
        last = lastPersistedVersion.get();
        version = latestVersionToPersist;
      }
      if (last != null && version.getVersion() <= last.getVersion()) {
        LOG.info("Attempt to persist version " + version
            + " smaller than last persisted version " + last);
        // Done, version is already persisted.
        version = last;
      } else {
        ImmutableList.Builder<WaveletDeltaRecord> deltas = ImmutableList.builder();
        HashedVersion v = (last == null) ? versionZero : last;
        do {
          WaveletDeltaRecord d = cachedDeltas.get(v);
          deltas.add(d);
          v = d.getResultingVersion();
        } while (v.getVersion() < version.getVersion());
        Preconditions.checkState(v.equals(version));
        deltasAccess.append(deltas.build());
      }
      synchronized (persistLock) {
        Preconditions.checkState(last == lastPersistedVersion.get(),
            "lastPersistedVersion changed while we were writing to storage");
        lastPersistedVersion.set(version);
        if (nextPersistTask != null) {
          persistExecutor.execute(nextPersistTask);
          nextPersistTask = null;
        } else {
          latestVersionToPersist = null;
          }
        }
      return null;
    }
  };

  /** Keyed by appliedAtVersion. */
  private final ConcurrentNavigableMap<HashedVersion, ByteStringMessage<ProtocolAppliedWaveletDelta>> appliedDeltas =
      new ConcurrentSkipListMap<HashedVersion, ByteStringMessage<ProtocolAppliedWaveletDelta>>();

  /** Keyed by appliedAtVersion. */
  private final ConcurrentNavigableMap<HashedVersion, WaveletDeltaRecord> cachedDeltas =
      new ConcurrentSkipListMap<HashedVersion, WaveletDeltaRecord>();

  /** Is null if the wavelet state is empty. */
  private WaveletData snapshot;

  /**
   * Last version persisted with a call to persist(), or null if never called.
   * It's an atomic reference so we can set in one thread (which
   * asynchronously writes deltas to storage) and read it in another,
   * simultaneously.
   */
  private final AtomicReference<HashedVersion> lastPersistedVersion;

  /**
   * Constructs a wavelet state with the given deltas and snapshot.
   * The deltas must be the contents of deltasAccess, and they
   * must be contiguous from version zero.
   * The snapshot must be the composition of the deltas, or null if there
   * are no deltas. The constructed object takes ownership of the
   * snapshot and will mutate it if appendDelta() is called.
   */
  @VisibleForTesting
  DeltaStoreBasedWaveletState(DeltaStore.DeltasAccess deltasAccess,
      List<WaveletDeltaRecord> deltas, WaveletData snapshot, Executor persistExecutor) {
    Preconditions.checkArgument(deltasAccess.isEmpty() == deltas.isEmpty());
    Preconditions.checkArgument(deltas.isEmpty() == (snapshot == null));
    this.persistExecutor = persistExecutor;
    this.versionZero = HASH_FACTORY.createVersionZero(deltasAccess.getWaveletName());
    this.deltasAccess = deltasAccess;
    this.snapshot = snapshot;
    this.lastPersistedVersion = new AtomicReference<HashedVersion>(deltasAccess.getEndVersion());
  }

  @Override
  public WaveletName getWaveletName() {
    return deltasAccess.getWaveletName();
  }

  @Override
  public ReadableWaveletData getSnapshot() {
    return snapshot;
  }

  @Override
  public HashedVersion getCurrentVersion() {
    return (snapshot == null) ? versionZero : snapshot.getHashedVersion();
  }

  @Override
  public HashedVersion getLastPersistedVersion() {
    HashedVersion version = lastPersistedVersion.get();
    return (version == null) ? versionZero : version;
  }

  @Override
  public HashedVersion getHashedVersion(long version) {
    final Entry<HashedVersion, WaveletDeltaRecord> cachedEntry =
        lookupCached(cachedDeltas, version);
    if (version == 0) {
      return versionZero;
    } else if (snapshot == null) {
      return null;
    } else if (version == snapshot.getVersion()) {
      return snapshot.getHashedVersion();
    } else {
      WaveletDeltaRecord delta;
      try {
        delta = lookup(version);
      } catch (IOException e) {
        throw new RuntimeIOException(new IOException(format("Version : %d", version), e));
      }
      if (delta == null && cachedEntry != null) {
        return cachedEntry.getKey();
      } else {
       return delta != null ? delta.getAppliedAtVersion() : null;
      }
    }
  }

  @Override
  public TransformedWaveletDelta getTransformedDelta(
      final HashedVersion beginVersion) {
    WaveletDeltaRecord delta = cachedDeltas.get(beginVersion);
    if (delta != null) {
      return delta.getTransformedDelta();
    } else {
      WaveletDeltaRecord nowDelta;
      try {
        nowDelta = lookup(beginVersion.getVersion());
      } catch (IOException e) {
        throw new RuntimeIOException(new IOException(format("Begin version : %s",
            beginVersion.toString()), e));
      }
      return nowDelta != null ? nowDelta.getTransformedDelta() : null;
    }
  }

  @Override
  public TransformedWaveletDelta getTransformedDeltaByEndVersion(final HashedVersion endVersion) {
    Preconditions.checkArgument(endVersion.getVersion() > 0, "end version %s is not positive",
        endVersion);
    Entry<HashedVersion, WaveletDeltaRecord> transformedEntry =
        cachedDeltas.lowerEntry(endVersion);
    final WaveletDeltaRecord cachedDelta =
        transformedEntry != null ? transformedEntry.getValue() : null;
    if (snapshot == null) {
      return null;
    } else {
      WaveletDeltaRecord deltaRecord = getDeltaRecordByEndVersion(endVersion);
      TransformedWaveletDelta delta;
      if (deltaRecord == null && cachedDelta != null
          && cachedDelta.getResultingVersion().equals(endVersion)) {
        delta = cachedDelta.getTransformedDelta();
      } else {
        delta = deltaRecord != null ? deltaRecord.getTransformedDelta() : null;
      }
      return delta;
    }
  }

  @Override
  public void getTransformedDeltaHistory(final HashedVersion startVersion,
    final HashedVersion endVersion, final Receiver<TransformedWaveletDelta> receiver) {
    try {
      readDeltasInRange(deltasAccess, cachedDeltas, startVersion, endVersion,
          new Receiver<WaveletDeltaRecord>() {
            @Override
            public boolean put(WaveletDeltaRecord delta) {
              return receiver.put(delta.getTransformedDelta());
            }
          });
    } catch (IOException e) {
      throw new RuntimeIOException(new IOException(format("Start version : %s, end version: %s",
          startVersion.toString(), endVersion.toString()), e));
    }
  }

  @Override
  public ByteStringMessage<ProtocolAppliedWaveletDelta> getAppliedDelta(
      HashedVersion beginVersion) {
    WaveletDeltaRecord delta = cachedDeltas.get(beginVersion);
    if (delta != null) {
      return delta.getAppliedDelta();
    } else {
      WaveletDeltaRecord record = null;
      try {
        record = lookup(beginVersion.getVersion());
      } catch (IOException e) {
        throw new RuntimeIOException(new IOException(format("Begin version : %s",
            beginVersion.toString()), e));
      }
      return record != null ? record.getAppliedDelta() : null;
    }
  }

  @Override
  public ByteStringMessage<ProtocolAppliedWaveletDelta> getAppliedDeltaByEndVersion(
      final HashedVersion endVersion) {
    Preconditions.checkArgument(endVersion.getVersion() > 0,
        "end version %s is not positive", endVersion);
    Entry<HashedVersion, WaveletDeltaRecord> appliedEntry =
        cachedDeltas.lowerEntry(endVersion);
    final ByteStringMessage<ProtocolAppliedWaveletDelta> cachedDelta =
        appliedEntry != null ? appliedEntry.getValue().getAppliedDelta() : null;
    WaveletDeltaRecord deltaRecord = getDeltaRecordByEndVersion(endVersion);
    ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta;
    if (deltaRecord == null && isDeltaBoundary(endVersion)) {
      appliedDelta = cachedDelta;
    } else {
      appliedDelta = deltaRecord != null ? deltaRecord.getAppliedDelta() : null;
    }
    return appliedDelta;
  }

  @Override
  public void getAppliedDeltaHistory(HashedVersion startVersion, HashedVersion endVersion,
      final Receiver<ByteStringMessage<ProtocolAppliedWaveletDelta>> receiver) {
    Preconditions.checkArgument(startVersion.getVersion() < endVersion.getVersion());
    try {
      readDeltasInRange(deltasAccess, cachedDeltas, startVersion, endVersion, new Receiver<WaveletDeltaRecord>() {
        @Override
        public boolean put(WaveletDeltaRecord delta) {
          return receiver.put(delta.getAppliedDelta());
        }
      });
    } catch (IOException e) {
      throw new RuntimeIOException(new IOException(format("Start version : %s, end version: %s",
          startVersion.toString(), endVersion.toString()), e));
    }
  }

  @Override
  public void appendDelta(WaveletDeltaRecord deltaRecord)
      throws OperationException {
    HashedVersion currentVersion = getCurrentVersion();
    Preconditions.checkArgument(currentVersion.equals(deltaRecord.getAppliedAtVersion()),
        "Applied version %s doesn't match current version %s", deltaRecord.getAppliedAtVersion(),
        currentVersion);

    if (deltaRecord.getAppliedAtVersion().getVersion() == 0) {
      Preconditions.checkState(lastPersistedVersion.get() == null);
      snapshot = WaveletDataUtil.buildWaveletFromFirstDelta(getWaveletName(), deltaRecord.getTransformedDelta());
    } else {
      WaveletDataUtil.applyWaveletDelta(deltaRecord.getTransformedDelta(), snapshot);
    }

    // Now that we built the snapshot without any exceptions, we record the delta.
    cachedDeltas.put(deltaRecord.getAppliedAtVersion(), deltaRecord);
  }

  @Override
  public ListenableFuture<Void> persist(final HashedVersion version) {
    Preconditions.checkArgument(version.getVersion() > 0,
        "Cannot persist non-positive version %s", version);
    Preconditions.checkArgument(isDeltaBoundary(version),
        "Version to persist %s matches no delta", version);
    synchronized (persistLock) {
      if (latestVersionToPersist != null) {
        // There's a persist task in flight.
        if (version.getVersion() <= latestVersionToPersist.getVersion()) {
          LOG.info("Attempt to persist version " + version
              + " smaller than last version requested " + latestVersionToPersist);
        } else {
          latestVersionToPersist = version;
        }
        if (nextPersistTask == null) {
          nextPersistTask = ListenableFutureTask.<Void>create(persisterTask);
        }
        return nextPersistTask;
      } else {
        latestVersionToPersist = version;
        ListenableFutureTask<Void> resultTask = ListenableFutureTask.<Void>create(persisterTask);
        persistExecutor.execute(resultTask);
        return resultTask;
      }
    }
  }

  @Override
  public void flush(HashedVersion version) {
    cachedDeltas.remove(cachedDeltas.lowerKey(version));
    if (LOG.isFineLoggable()) {
      LOG.fine("Flushed deltas up to version " + version);
    }
  }

  @Override
  public void close() {
  }

  /**
   * @return An entry keyed by a hashed version with the given version number,
   *         if any, otherwise null.
   */
  private WaveletDeltaRecord lookup(long version) throws IOException {
    return deltasAccess.getDelta(version);
  }

  private WaveletDeltaRecord getDeltaRecordByEndVersion(HashedVersion endVersion) {
    long version = endVersion.getVersion();
    try {
      return deltasAccess.getDeltaByEndVersion(version);
    } catch (IOException e) {
      throw new RuntimeIOException(new IOException(format("Version : %d", version), e));
    }
  }

  private boolean isDeltaBoundary(HashedVersion version) {
    Preconditions.checkNotNull(version, "version is null");
    return version.equals(getCurrentVersion()) || cachedDeltas.containsKey(version);
  }
}
TOP

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

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.