com.apple.foundationdb.tuple.Versionstamp Maven / Gradle / Ivy
/*
* Versionstamp.java
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2013-2018 Apple Inc. and the FoundationDB project authors
*
* 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 com.apple.foundationdb.tuple;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
/**
* Used to represent values written by versionstamp operations with a {@link Tuple}.
* This wraps a single array which should contain twelve bytes. The first ten bytes
* are the "transaction" version, and they are usually assigned by the database
* in such a way that all transactions receive a different version that is consistent
* with a serialization order of the transactions within the database. (One can
* use the {@link com.apple.foundationdb.Transaction#getVersionstamp() Transaction.getVersionstamp()}
* method to retrieve this version from a {@code Transaction}.) This also implies that the
* transaction version of newly committed transactions will be monotonically increasing
* over time. The final two bytes are the "user" version and should be set by the client.
* This allows the user to use this class to impose a total order of items across multiple
* transactions in the database in a consistent and conflict-free way. The user can elect to
* ignore this parameter by instantiating the class with the parameterless {@link #incomplete() incomplete()}
* and one-parameter {@link #complete(byte[]) complete} static initializers. If they do so,
* then versions are written with a default (constant) user version.
*
*
* All {@code Versionstamp}s can exist in one of two states: "incomplete" and "complete".
* An "incomplete" {@code Versionstamp} is a {@code Versionstamp} that has not been
* initialized with a meaningful transaction version. For example, this might be used
* with a {@code Versionstamp} that one wants to fill in with the current transaction's
* version information. A "complete" {@code Versionstamp}, in contradistinction, is one
* that has been assigned a meaningful transaction version. This is usually the
* case if one is reading back a {@code Versionstamp} from the database.
*
*
*
* Example usage might be to do something like the following:
*
*
*
*
* CompletableFuture<byte[]> trVersionFuture = db.run((Transaction tr) -> {
* // The incomplete Versionstamp will be overwritten with tr's version information when committed.
* Tuple t = Tuple.from("prefix", Versionstamp.incomplete());
* tr.mutate(MutationType.SET_VERSIONSTAMPED_KEY, t.packWithVersionstamp(), new byte[0]);
* return tr.getVersionstamp();
* });
*
* byte[] trVersion = trVersionFuture.get();
*
* Versionstamp v = db.run((Transaction tr) -> {
* Subspace subspace = new Subspace(Tuple.from("prefix"));
* byte[] serialized = tr.getRange(subspace.range(), 1).iterator().next().getKey();
* Tuple t = subspace.unpack(serialized);
* return t.getVersionstamp(0);
* });
*
* assert v.equals(Versionstamp.complete(trVersion));
*
*
*
*
* Here, an incomplete {@code Versionstamp} is packed and written to the database with
* the {@link com.apple.foundationdb.MutationType#SET_VERSIONSTAMPED_KEY SET_VERSIONSTAMPED_KEY}
* {@code MutationType}. After committing, we then attempt to read back the same key that
* we just wrote. Then we verify the invariant that the deserialized {@link Versionstamp} is
* the same as a complete {@code Versionstamp} instance created from the first transaction's
* version information.
*
*/
public class Versionstamp implements Comparable {
/**
* Length of a serialized {@code Versionstamp} instance when converted into a byte array.
*/
public static final int LENGTH = 12;
private static final byte[] UNSET_TRANSACTION_VERSION = {(byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff,
(byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff};
private final boolean complete;
private final byte[] versionBytes;
/**
* From a byte array, unpack the user version starting at the given position.
* This assumes that the bytes are stored in big-endian order as an unsigned
* short, which is the way the user version is serialized in packed {@code Versionstamp}s.
*
* @param bytes byte array including user version
* @param pos starting position of user version
* @return the unpacked user version included in the array
*/
public static int unpackUserVersion(byte[] bytes, int pos) {
return (((int)bytes[pos] & 0xff) << 8) | ((int)bytes[pos + 1] & 0xff);
}
/**
* Creates a {@code Versionstamp} instance based on the given byte array
* representation. This follows the same format as that used by
* the main constructor, but the completeness of the {@code Versionstamp}
* is instead automatically determined by comparing its transaction version
* with the value used to indicate an unset transaction version.
*
* @param versionBytes byte array representation of {@code Versionstamp}
* @return equivalent instantiated {@code Versionstamp} object
*/
public static Versionstamp fromBytes(byte[] versionBytes) {
if(versionBytes.length != LENGTH) {
throw new IllegalArgumentException("Versionstamp bytes must have length " + LENGTH);
}
boolean complete = false;
for(int i = 0; i < UNSET_TRANSACTION_VERSION.length; i++) {
if(versionBytes[i] != UNSET_TRANSACTION_VERSION[i]) {
complete = true;
}
}
return new Versionstamp(complete, versionBytes);
}
/**
* Creates an incomplete {@code Versionstamp} instance with the given
* user version. The provided user version must fit within an unsigned
* short. When converted into a byte array, the bytes for the transaction
* version will be filled in with dummy bytes to be later filled
* in at transaction commit time.
*
* @param userVersion intra-transaction portion of version (set by user code)
* @return an incomplete {@code Versionstamp} with the given user version
*/
public static Versionstamp incomplete(int userVersion) {
if(userVersion < 0 || userVersion > 0xffff) {
throw new IllegalArgumentException("Local version must fit in unsigned short");
}
ByteBuffer bb = ByteBuffer.allocate(LENGTH).order(ByteOrder.BIG_ENDIAN);
bb.put(UNSET_TRANSACTION_VERSION);
bb.putShort((short)userVersion);
return new Versionstamp(false, bb.array());
}
/**
* Creates an incomplete {@code Versionstamp} instance with the default user
* version. When converted into a byte array, the bytes for the transaction
* version will be filled in with dummy bytes to be later filled in at
* transaction commit time. If multiple keys are created using the returned
* {@code Versionstamp} within the same transaction, then all of those
* keys will have the same version, but it will provide an ordering between
* different transactions if that is all that is required.
*
* @return an incomplete {@code Versionstamp} with the default user version
*/
public static Versionstamp incomplete() {
return incomplete(0);
}
/**
* Creates a complete {@code Versionstamp} instance with the given
* transaction and user versions. The provided transaction version must have
* exactly 10 bytes, and the user version must fit within an unsigned
* short.
*
* @param trVersion inter-transaction portion of version (set by the database)
* @param userVersion intra-transaction portion of version (set by user code)
* @return a complete {@code Versionstamp} assembled from the given parts
*/
public static Versionstamp complete(byte[] trVersion, int userVersion) {
if(trVersion.length != UNSET_TRANSACTION_VERSION.length) {
throw new IllegalArgumentException("Global version has invalid length " + trVersion.length);
}
if(userVersion < 0 || userVersion > 0xffff) {
throw new IllegalArgumentException("Local version must fit in unsigned short");
}
ByteBuffer bb = ByteBuffer.allocate(LENGTH).order(ByteOrder.BIG_ENDIAN);
bb.put(trVersion);
bb.putShort((short)userVersion);
return new Versionstamp(true, bb.array());
}
/**
* Creates a complete {@code Versionstamp} instance with the given
* transaction and default user versions. The provided transaction version
* must have exactly 10 bytes.
*
* @param trVersion inter-transaction portion of version (set by the database)
* @return a complete {@code Versionstamp} assembled from the given transaction
* version and the default user version
*/
public static Versionstamp complete(byte[] trVersion) {
return complete(trVersion, 0);
}
private Versionstamp(boolean complete, byte[] versionBytes) {
if (versionBytes.length != LENGTH) {
throw new IllegalArgumentException("Versionstamp bytes must have length " + LENGTH);
}
this.complete = complete;
this.versionBytes = versionBytes;
}
/**
* Whether this {@code Versionstamp}'s transaction version is
* meaningful. The database will assign each transaction a different transaction
* version. A {@code Versionstamp} is considered to be "complete" if its
* transaction version is one of those database-assigned versions rather than
* just dummy bytes. If one uses this class with our
* {@link com.apple.foundationdb.MutationType#SET_VERSIONSTAMPED_KEY SET_VERSIONSTAMPED_KEY}
* mutation, then the appropriate bytes will be filled in within the database
* during a successful commit.
*
* @return whether the transaction version has been set
*/
public boolean isComplete() {
return complete;
}
/**
* Retrieve a byte-array representation of this {@code Versionstamp}.
* This representation can then be serialized and added to the database.
* If this {@code Versionstamp} is not complete, the first 10 bytes (representing the
* transaction version) will not be meaningful and one should probably use with the
* {@link com.apple.foundationdb.MutationType#SET_VERSIONSTAMPED_KEY SET_VERSIONSTAMPED_KEY}
* mutation.
*
* Warning: For performance reasons, this method does not create a copy of
* its underlying data array. As a result, it is dangerous to modify the
* return value of this function.
*
* @return byte representation of this Versionstamp
*/
public byte[] getBytes() {
return versionBytes;
}
/**
* Retrieve the portion of this Versionstamp
that is set by
* the database. These 10 bytes are what provide an ordering between different commits.
*
* @return transaction version of this Versionstamp
*/
public byte[] getTransactionVersion() {
byte[] ret = new byte[LENGTH - 2];
System.arraycopy(versionBytes, 0, ret, 0, ret.length);
return ret;
}
/**
* Retrieve the portion of this Versionstamp
that is set
* by the user. This integer is what provides an ordering within
* a single commit.
*
* @return user version of this Versionstamp
*/
public int getUserVersion() {
return unpackUserVersion(versionBytes, LENGTH - 2);
}
/**
* Generate a human-readable representation of this {@code Versionstamp}. It contains
* information as to whether this {@code Versionstamp} is incomplete or not, what
* its transaction version is (if the {@code Versionstamp} is complete), and what its
* user version is.
*
* @return a human-readable representation of this {@code Versionstamp}
*/
@Override
public String toString() {
if(complete) {
return "Versionstamp(" + ByteArrayUtil.printable(getTransactionVersion()) + " " + getUserVersion() + ")";
} else {
return "Versionstamp( " + getUserVersion() + ")";
}
}
/**
* Compares two {@code Versionstamp} instances in a manner consistent with their
* key order when serialized in the database as keys. The rules for comparison are:
*
*
* - All complete {@code Versionstamp}s sort before incomplete {@code Versionstamp}s
* -
* Two complete {@code Versionstamp}s will sort based on unsigned lexicographic comparison
* of their byte representations.
*
* - Two incomplete {@code Versionstamp}s will sort based on their user versions.
*
*
* @param other {@code Versionstamp} instance to compare against
* @return -1 if this {@code Versionstamp} is smaller than {@code other}, 1 if it is bigger, and
* 0 if it is equal
*/
@Override
public int compareTo(Versionstamp other) {
if(complete && other.complete) {
return ByteArrayUtil.compareUnsigned(this.versionBytes, other.versionBytes);
} else if(complete) {
// The other is incomplete and thus greater.
return -1;
} else if(other.complete) {
// This instance is incomplete and thus greater.
return 1;
} else {
return Integer.compare(getUserVersion(), other.getUserVersion());
}
}
/**
* Check another object for equality. This will return true
if and only
* if the o
parameter is of type Versionstamp
and has
* the same completion status and byte representation.
*
* @param o object to compare for equality
* @return whether the passed object is an equivalent Versionstamp
*/
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (this == o) {
return true;
}
if (!(o instanceof Versionstamp)) {
return false;
}
Versionstamp that = (Versionstamp)o;
return this.isComplete() == that.isComplete() && Arrays.equals(this.getBytes(), that.getBytes());
}
/**
* Hash code for this Versionstamp
. It is based off of the hash
* code for the underlying data array.
*
* @return hash-table compliant hash code for this instance
*/
public int hashCode() {
return Arrays.hashCode(getBytes());
}
}