/** * 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.Maps; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFutureTask; 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.NavigableMap; import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; /** * Simplistic {@link DeltaStore}-backed wavelet state implementation * which keeps the entire delta history in memory. * * TODO(soren): only keep in memory what's not persisted * * 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 * * TODO(soren): refine the persist() logic to make it batch successive * writes to storage, when write latency exceeds the intervals between * calls to persist() * * @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(); } }; /** * 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 { // Note that the logic in persist() depends on persistExecutor being single-threaded. // TODO(soren): finesse the logic in persist() so it // doesn't require it to be single-threaded, because it would be useful to be // able to use a shared executor with a thread-count set to the appropriate level // of write parallelism for the storage subsystem. 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 */ private static ImmutableList<WaveletDeltaRecord> readAll(WaveletDeltaRecordReader reader) throws IOException{ Preconditions.checkArgument(!reader.isEmpty()); ImmutableList.Builder<WaveletDeltaRecord> result = ImmutableList.builder(); HashedVersion endVersion = reader.getEndVersion(); long version = 0; while (version < endVersion.getVersion()) { WaveletDeltaRecord delta = reader.getDelta(version); result.add(delta); version = delta.getResultingVersion().getVersion(); } return result.build(); } /** * @return An entry keyed by a hashed version with the given version number, * if any, otherwise null. */ private static <T> Map.Entry<HashedVersion, T> lookup( 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; } private final Executor persistExecutor; private final HashedVersion versionZero; private final DeltaStore.DeltasAccess deltasAccess; /** Keyed by appliedAtVersion. */ private final NavigableMap<HashedVersion, ByteStringMessage<ProtocolAppliedWaveletDelta>> appliedDeltas = Maps.newTreeMap(); /** Keyed by appliedAtVersion. */ private final NavigableMap<HashedVersion, TransformedWaveletDelta> transformedDeltas = Maps.newTreeMap(); /** 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; for (WaveletDeltaRecord delta : deltas) { HashedVersion hashedVersion = delta.getAppliedAtVersion(); appliedDeltas.put(hashedVersion, delta.getAppliedDelta()); transformedDeltas.put(hashedVersion, delta.getTransformedDelta()); } 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) { if (version == 0) { return versionZero; } else if (snapshot == null) { return null; } else if (version == snapshot.getVersion()) { return snapshot.getHashedVersion(); } else { Map.Entry<HashedVersion, TransformedWaveletDelta> entry = lookup(transformedDeltas, version); return (entry == null) ? null : entry.getKey(); } } @Override public TransformedWaveletDelta getTransformedDelta(HashedVersion beginVersion) { return transformedDeltas.get(beginVersion); } @Override public TransformedWaveletDelta getTransformedDeltaByEndVersion(HashedVersion endVersion) { Preconditions.checkArgument(endVersion.getVersion() > 0, "end version %s is not positive", endVersion); if (snapshot == null) { return null; } else if (endVersion.equals(snapshot.getHashedVersion())) { return transformedDeltas.lastEntry().getValue(); } else { TransformedWaveletDelta delta = transformedDeltas.lowerEntry(endVersion).getValue(); return delta.getResultingVersion().equals(endVersion) ? delta : null; } } @Override public DeltaSequence getTransformedDeltaHistory(HashedVersion startVersion, HashedVersion endVersion) { Preconditions.checkArgument(startVersion.getVersion() < endVersion.getVersion(), "Start version %s should be smaller than end version %s", startVersion, endVersion); NavigableMap<HashedVersion, TransformedWaveletDelta> deltas = transformedDeltas.subMap(startVersion, true, endVersion, false); return (!deltas.isEmpty() && deltas.firstKey().equals(startVersion) && deltas.lastEntry().getValue().getResultingVersion().equals(endVersion)) ? DeltaSequence.of(deltas.values()) : null; } @Override public ByteStringMessage<ProtocolAppliedWaveletDelta> getAppliedDelta( HashedVersion beginVersion) { return appliedDeltas.get(beginVersion); } @Override public ByteStringMessage<ProtocolAppliedWaveletDelta> getAppliedDeltaByEndVersion( HashedVersion endVersion) { Preconditions.checkArgument(endVersion.getVersion() > 0, "end version %s is not positive", endVersion); return isDeltaBoundary(endVersion) ? appliedDeltas.lowerEntry(endVersion).getValue() : null; } @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; } @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); // The following logic relies on persistExecutor being single-threaded, // so no two tasks execute in parallel. ListenableFutureTask<Void> resultTask = new ListenableFutureTask<Void>( new Callable<Void>() { @Override public Void call() throws PersistenceException { HashedVersion last = lastPersistedVersion.get(); 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 } 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()); Preconditions.checkState(last == lastPersistedVersion.get(), "lastPersistedVersion changed while we were writing to storage"); lastPersistedVersion.set(version); } return null; } }); persistExecutor.execute(resultTask); return resultTask; } @Override public void close() { } private boolean isDeltaBoundary(HashedVersion version) { Preconditions.checkNotNull(version, "version is null"); return version.equals(getCurrentVersion()) || transformedDeltas.containsKey(version); } }