/** * 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); } }