/** * Finds range of server deltas needed to transform against, then transforms all client ops * against the server ops. */ private VersionedWaveletDelta transformSubmittedDelta( WaveletDelta submittedDelta, HashedVersion appliedVersion) throws OperationException, InvalidHashException { NavigableSet<VersionedWaveletDelta> serverDeltas = deserializedTransformedDeltas.tailSet( deserializedTransformedDeltas.floor( emptyDeserializedDeltaAtVersion(appliedVersion.getVersion())), true); if (serverDeltas.size() == 0) { LOG.warning("Got empty server set, but not sumbitting to head! " + submittedDelta); // Not strictly an invalid hash, but it's a related issue throw new InvalidHashException("Cannot submit to head"); } // Confirm that the target version/hash of this delta is valid. if (!serverDeltas.first().version.equals(appliedVersion)) { LOG.warning( "Mismatched hashes: expected: " + serverDeltas.first().version + " got: " + appliedVersion); // Don't leak the hash to the client in the error message. throw new InvalidHashException("Mismatched hashes at version " + appliedVersion.getVersion()); } ParticipantId clientAuthor = submittedDelta.getAuthor(); List<WaveletOperation> clientOps = submittedDelta.getOperations(); for (VersionedWaveletDelta d : serverDeltas) { // If the client delta transforms to nothing before we've traversed all the server // deltas, return the version at which the delta was obliterated (rather than the // current version) to ensure that delta submission is idempotent. if (clientOps.isEmpty()) { return new VersionedWaveletDelta(new WaveletDelta(clientAuthor, clientOps), d.version); } ParticipantId serverAuthor = d.delta.getAuthor(); List<WaveletOperation> serverOps = d.delta.getOperations(); if (clientAuthor.equals(serverAuthor) && clientOps.equals(serverOps)) { return d; } clientOps = transformOps(clientOps, clientAuthor, serverOps, serverAuthor); } return new VersionedWaveletDelta(new WaveletDelta(clientAuthor, clientOps), currentVersion); }
/** * Transform a wavelet delta if it has been submitted against a different head (currentVersion). * Must be called with write lock held. * * @param delta to possibly transform * @param appliedVersion that the delta is applied to * @return the transformed delta and the version it was applied at (the version is the current * version of the wavelet, unless the delta is a duplicate in which case it is the version at * which it was originally applied) * @throws InvalidHashException if submitting against same version but different hash * @throws OperationException if transformation fails */ protected VersionedWaveletDelta maybeTransformSubmittedDelta( WaveletDelta delta, HashedVersion appliedVersion) throws InvalidHashException, OperationException { if (appliedVersion.equals(currentVersion)) { // Applied version is the same, we're submitting against head, don't need to do OT return new VersionedWaveletDelta(delta, appliedVersion); } else { // Not submitting against head, we need to do OT, but check the versions really are different if (appliedVersion.getVersion() == currentVersion.getVersion()) { LOG.warning( "Same version (" + currentVersion.getVersion() + ") but different hashes (" + appliedVersion + "/" + currentVersion + ")"); throw new InvalidHashException( "Different hash, same version: " + currentVersion.getVersion()); } else { return transformSubmittedDelta(delta, appliedVersion); } } }
/** * Returns the applied delta that was applied at a given hashed version. * * @param version the version to look up * @return the applied delta applied at the specified hashed version */ protected ByteStringMessage<ProtocolAppliedWaveletDelta> lookupAppliedDelta( HashedVersion version) { return appliedDeltas.floor(emptyAppliedDeltaAtVersion(version.getVersion())); }