/**
* Copyright 2010 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.box.server.waveserver;
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.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.google.gxp.compiler.io.RuntimeIOException;
import org.waveprotocol.box.common.DeltaSequence;
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.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Set;
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)
*/
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);
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)
throws IOException {
long startVersion = 0;
long endVersion = reader.getEndVersion().getVersion();
return readDeltasInRange(reader, startVersion, endVersion);
}
private static ImmutableList<WaveletDeltaRecord> readDeltasInRange(
final WaveletDeltaRecordReader reader, final long startVersion, final long endVersion) throws IOException {
Preconditions.checkArgument(!reader.isEmpty());
ImmutableList.Builder<WaveletDeltaRecord> result = ImmutableList.builder();
long i = startVersion;
while (i < endVersion) {
WaveletDeltaRecord delta;
delta = reader.getDelta(i);
result.add(delta);
i = delta.getResultingVersion().getVersion();
}
return result.build();
}
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 =
new WaveletDeltaRecord(v, appliedDeltas.get(v), transformedDeltas.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, TransformedWaveletDelta> transformedDeltas =
new ConcurrentSkipListMap<HashedVersion, TransformedWaveletDelta>();
/** 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, TransformedWaveletDelta> cachedEntry =
lookupCached(transformedDeltas, 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(e);
}
if (delta == null && cachedEntry != null) {
return cachedEntry.getKey();
} else {
return delta != null ? delta.getAppliedAtVersion() : null;
}
}
}
@Override
public TransformedWaveletDelta getTransformedDelta(
final HashedVersion beginVersion) {
TransformedWaveletDelta delta = transformedDeltas.get(beginVersion);
if (delta != null) {
return delta;
} else {
WaveletDeltaRecord nowDelta;
try {
nowDelta = lookup(beginVersion.getVersion());
} catch (IOException e) {
throw new RuntimeIOException(e);
}
return nowDelta != null ? nowDelta.transformed : null;
}
}
@Override
public TransformedWaveletDelta getTransformedDeltaByEndVersion(final HashedVersion endVersion) {
Preconditions.checkArgument(endVersion.getVersion() > 0, "end version %s is not positive",
endVersion);
Entry<HashedVersion, TransformedWaveletDelta> transformedEntry =
transformedDeltas.lowerEntry(endVersion);
final TransformedWaveletDelta 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;
} else {
delta = deltaRecord != null ? deltaRecord.getTransformedDelta() : null;
}
return delta;
}
}
@Override
public DeltaSequence getTransformedDeltaHistory(final HashedVersion startVersion,
final HashedVersion endVersion) {
Preconditions.checkArgument(startVersion.getVersion() < endVersion.getVersion(),
"Start version %s should be smaller than end version %s", startVersion, endVersion);
// The history deltas can be either in the memory - waiting to be persisted,
// or already persisted. We take both and merge into one list.
final NavigableMap<HashedVersion, TransformedWaveletDelta> cachedDeltas = Maps.newTreeMap();
cachedDeltas.putAll(transformedDeltas.subMap(startVersion, true, endVersion, false));
ImmutableList<WaveletDeltaRecord> persistedDeltas;
try {
persistedDeltas =
readDeltasInRange(deltasAccess, startVersion.getVersion(), endVersion.getVersion());
} catch (IOException e) {
throw new RuntimeIOException(e);
}
NavigableMap<HashedVersion, TransformedWaveletDelta> allTransformedDeltasMap =
Maps.newTreeMap();
allTransformedDeltasMap.putAll(cachedDeltas);
for (WaveletDeltaRecord d : persistedDeltas) {
allTransformedDeltasMap.put(d.getAppliedAtVersion(), d.getTransformedDelta());
}
DeltaSequence nowDeltaSequence;
if (!allTransformedDeltasMap.isEmpty()
&& allTransformedDeltasMap.firstKey().equals(startVersion)
&& allTransformedDeltasMap.lastEntry().getValue().getResultingVersion().equals(endVersion)) {
List<TransformedWaveletDelta> cachedAndPersitentDeltasList =
Lists.newArrayList(allTransformedDeltasMap.values());
nowDeltaSequence = DeltaSequence.of(cachedAndPersitentDeltasList);
} else {
nowDeltaSequence = null;
}
return nowDeltaSequence;
}
@Override
public ByteStringMessage<ProtocolAppliedWaveletDelta> getAppliedDelta(
HashedVersion beginVersion) {
ByteStringMessage<ProtocolAppliedWaveletDelta> delta = appliedDeltas.get(beginVersion);
if (delta != null) {
return delta;
} else {
WaveletDeltaRecord record = null;
try {
record = lookup(beginVersion.getVersion());
} catch (IOException e) {
new RuntimeIOException(e);
}
return record != null ? record.applied : null;
}
}
@Override
public ByteStringMessage<ProtocolAppliedWaveletDelta> getAppliedDeltaByEndVersion(
final HashedVersion endVersion) {
Preconditions.checkArgument(endVersion.getVersion() > 0,
"end version %s is not positive", endVersion);
Entry<HashedVersion, ByteStringMessage<ProtocolAppliedWaveletDelta>> appliedEntry =
appliedDeltas.lowerEntry(endVersion);
final ByteStringMessage<ProtocolAppliedWaveletDelta> cachedDelta =
appliedEntry != null ? appliedEntry.getValue() : 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 Collection<ByteStringMessage<ProtocolAppliedWaveletDelta>> getAppliedDeltaHistory(
HashedVersion startVersion, HashedVersion endVersion) {
Preconditions.checkArgument(startVersion.getVersion() < endVersion.getVersion());
return (isDeltaBoundary(startVersion) && isDeltaBoundary(endVersion))
? appliedDeltas.subMap(startVersion, endVersion).values()
: null;
}
public Collection<ByteStringMessage<ProtocolAppliedWaveletDelta>> getAppliedDeltaHistory1(
final HashedVersion startVersion, final HashedVersion endVersion) {
Preconditions.checkArgument(startVersion.getVersion() < endVersion.getVersion());
final Set<ByteStringMessage<ProtocolAppliedWaveletDelta>> allDeltas = Sets.newHashSet();
allDeltas.addAll(appliedDeltas.subMap(startVersion, endVersion).values());
ImmutableList<WaveletDeltaRecord> persistedDeltas;
try {
persistedDeltas =
readDeltasInRange(deltasAccess, startVersion.getVersion(), endVersion.getVersion());
} catch (IOException e) {
throw new RuntimeIOException(e);
}
for (WaveletDeltaRecord d : persistedDeltas) {
allDeltas.add(d.getAppliedDelta());
}
Collection<ByteStringMessage<ProtocolAppliedWaveletDelta>> deltaCollection =
Lists.newArrayList();
if (isDeltaBoundary(startVersion) && isDeltaBoundary(endVersion)) {
for (ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta : allDeltas) {
deltaCollection.add(appliedDelta);
}
}
return deltaCollection;
}
@Override
public void appendDelta(HashedVersion appliedAtVersion,
TransformedWaveletDelta transformedDelta,
ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta)
throws OperationException {
HashedVersion currentVersion = getCurrentVersion();
Preconditions.checkArgument(currentVersion.equals(appliedAtVersion),
"Applied version %s doesn't match current version %s", appliedAtVersion, currentVersion);
if (appliedAtVersion.getVersion() == 0) {
Preconditions.checkState(lastPersistedVersion.get() == null);
snapshot = WaveletDataUtil.buildWaveletFromFirstDelta(getWaveletName(), transformedDelta);
} else {
WaveletDataUtil.applyWaveletDelta(transformedDelta, snapshot);
}
// Now that we built the snapshot without any exceptions, we record the delta.
transformedDeltas.put(appliedAtVersion, transformedDelta);
appliedDeltas.put(appliedAtVersion, appliedDelta);
}
@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 = new ListenableFutureTask<Void>(persisterTask);
}
return nextPersistTask;
} else {
latestVersionToPersist = version;
ListenableFutureTask<Void> resultTask = new ListenableFutureTask<Void>(persisterTask);
persistExecutor.execute(resultTask);
return resultTask;
}
}
}
@Override
public void flush(HashedVersion version) {
transformedDeltas.remove(transformedDeltas.lowerKey(version));
appliedDeltas.remove(appliedDeltas.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(e);
}
}
private boolean isDeltaBoundary(HashedVersion version) {
Preconditions.checkNotNull(version, "version is null");
return version.equals(getCurrentVersion()) || transformedDeltas.containsKey(version);
}
}