blob: 0e86797fa973f0068a63d3f5d02d5885321b176d [file] [log] [blame]
/*
* 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.apache.jackrabbit.oak.plugins.document;
import org.apache.jackrabbit.oak.cache.CacheValue;
import org.apache.jackrabbit.oak.plugins.document.util.Utils;
import org.apache.jackrabbit.oak.stats.Clock;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A revision.
*/
public final class Revision implements CacheValue {
//Extra 2 for those cases where counter or clusterId is 2 digit
final static int REV_STRING_APPROX_SIZE = Revision.newRevision(0).toString().length() + 2;
static final int SHALLOW_MEMORY_USAGE = 32;
private static volatile long lastTimestamp;
private static volatile long lastRevisionTimestamp;
private static volatile int lastRevisionCount;
/**
* The timestamp in milliseconds since 1970 (unlike in seconds as in
* MongoDB). The timestamp is local to the machine that generated the
* revision, such that timestamps of revisions can only be compared if the
* machine id is the same.
*/
private final long timestamp;
/**
* An incrementing counter, for commits that occur within the same
* millisecond.
*/
private final int counter;
/**
* The cluster id (the MongoDB machine id).
*/
private final int clusterId;
/**
* Whether this is a branch revision.
*/
private final boolean branch;
/** Only set for testing */
private static Clock clock;
/**
* <b>
* Only to be used for testing.
* Do Not Use Otherwise
* </b>
*
* @param c - the clock
*/
static void setClock(Clock c) {
checkNotNull(c);
clock = c;
lastTimestamp = clock.getTime();
lastRevisionTimestamp = clock.getTime();
}
/**
* <b>
* Only to be used for testing.
* Do Not Use Otherwise
* </b>
*/
static void resetClockToDefault() {
setClock(Clock.SIMPLE);
}
public Revision(long timestamp, int counter, int clusterId) {
this(timestamp, counter, clusterId, false);
}
public Revision(long timestamp, int counter, int clusterId, boolean branch) {
this.timestamp = timestamp;
this.counter = counter;
this.clusterId = clusterId;
this.branch = branch;
}
/**
* Compare the time part of two revisions. If they contain the same time,
* the counter is compared.
* <p>
* This method requires that both revisions are from the same cluster node.
*
* @param other the other revision
* @return -1 if this revision occurred earlier, 1 if later, 0 if equal
* @throws IllegalArgumentException if the cluster ids don't match
*/
int compareRevisionTime(Revision other) {
if (clusterId != other.clusterId) {
throw new IllegalArgumentException(
"Trying to compare revisions of different cluster ids: " +
this + " and " + other);
}
int comp = timestamp < other.timestamp ? -1 : timestamp > other.timestamp ? 1 : 0;
if (comp == 0) {
comp = counter < other.counter ? -1 : counter > other.counter ? 1 : 0;
}
return comp;
}
/**
* Compare the time part of two revisions. If they contain the same time,
* the counter is compared. If the counter is the same, the cluster ids are
* compared.
*
* @param other the other revision
* @return -1 if this revision occurred earlier, 1 if later, 0 if equal
*/
int compareRevisionTimeThenClusterId(Revision other) {
int comp = timestamp < other.timestamp ? -1 : timestamp > other.timestamp ? 1 : 0;
if (comp == 0) {
comp = counter < other.counter ? -1 : counter > other.counter ? 1 : 0;
}
if (comp == 0) {
comp = compareClusterId(other);
}
return comp;
}
/**
* Compare all components of two revisions.
*
* @param other the other revision
* @return -1, 0, or 1
*/
int compareTo(Revision other) {
int comp = compareRevisionTimeThenClusterId(other);
if (comp == 0) {
if (branch != other.branch) {
return branch ? -1 : 1;
}
}
return comp;
}
/**
* Compare the cluster node ids of both revisions.
*
* @param other the other revision
* @return -1 if this revision occurred earlier, 1 if later, 0 if equal
*/
int compareClusterId(Revision other) {
return clusterId < other.clusterId ? -1 : clusterId > other.clusterId ? 1 : 0;
}
/**
* Create a simple revision id. The format is similar to MongoDB ObjectId.
*
* @param clusterId the unique machineId + processId
* @return the unique revision id
*/
static Revision newRevision(int clusterId) {
long timestamp = getCurrentTimestamp();
int c;
synchronized (Revision.class) {
// need to check again, because threads
// could arrive inside the synchronized block
// out of order
if (timestamp < lastRevisionTimestamp) {
timestamp = lastRevisionTimestamp;
}
if (timestamp == lastRevisionTimestamp) {
c = ++lastRevisionCount;
} else {
lastRevisionTimestamp = timestamp;
lastRevisionCount = c = 0;
}
}
return new Revision(timestamp, c, clusterId);
}
/**
* Get the timestamp value of the current date and time. Within the same
* process, the returned value is never smaller than a previously returned
* value, even if the system time was changed.
*
* @return the timestamp
*/
public static long getCurrentTimestamp() {
long timestamp = System.currentTimeMillis();
if (clock != null) {
timestamp = clock.getTime();
}
if (timestamp < lastTimestamp) {
// protect against decreases in the system time,
// time machines, and other fluctuations in the time continuum
timestamp = lastTimestamp;
} else if (timestamp > lastTimestamp) {
lastTimestamp = timestamp;
}
return timestamp;
}
/**
* Get the timestamp difference between two revisions (r1 - r2) in
* milliseconds.
*
* @param r1 the first revision
* @param r2 the second revision
* @return the difference in milliseconds
*/
public static long getTimestampDifference(Revision r1, Revision r2) {
return r1.getTimestamp() - r2.getTimestamp();
}
public static Revision fromString(String rev) {
boolean isBranch = rev.charAt(0) == 'b';
int idx = isBranch ? 2 : 1;
if (rev.charAt(idx - 1) != 'r') {
throw new IllegalArgumentException(rev);
}
int len = rev.length();
// Parse timestamp
long timestamp = 0;
for (; idx < len; idx++) {
char c = rev.charAt(idx);
if (c == '-') {
break;
}
int digit = c >= 'a'? c - 'a' + 10 : c - '0';
timestamp = (timestamp << 4) + digit;
}
// Parse counter
int counter = 0;
for (idx++; idx < len; idx++) {
char c = rev.charAt(idx);
if (c == '-') {
break;
}
int digit = c >= 'a' ? c - 'a' + 10 : c - '0';
counter = (counter << 4) + digit;
}
// Parse clusterId
int clusterId = 0;
for (idx++; idx < len; idx++) {
char c = rev.charAt(idx);
int digit = c >= 'a' ? c - 'a' + 10 : c - '0';
clusterId = (clusterId << 4) + digit;
}
return new Revision(timestamp, counter, clusterId, isBranch);
}
@Override
public String toString() {
return toStringBuilder(new StringBuilder(REV_STRING_APPROX_SIZE)).toString();
}
/**
* Appends the string representation of this revision to the given
* StringBuilder.
*
* @param sb a StringBuilder.
* @return the StringBuilder instance passed to this method.
*/
public StringBuilder toStringBuilder(StringBuilder sb) {
if (branch) {
sb.append('b');
}
sb.append('r');
toHexString(sb, timestamp);
sb.append('-');
toHexString(sb, counter);
sb.append('-');
toHexString(sb, clusterId);
return sb;
}
public String toReadableString() {
StringBuilder buff = new StringBuilder();
buff.append("revision: \"").append(toString()).append("\"");
buff.append(", clusterId: ").append(clusterId);
buff.append(", time: \"").
append(Utils.timestampToString(timestamp)).
append("\"");
if (counter > 0) {
buff.append(", counter: ").append(counter);
}
if (branch) {
buff.append(", branch: true");
}
return buff.toString();
}
/**
* Get the timestamp in milliseconds since 1970.
*
* @return the timestamp
*/
public long getTimestamp() {
return timestamp;
}
public int getCounter() {
return counter;
}
/**
* @return <code>true</code> if this is a branch revision, otherwise
* <code>false</code>.
*/
public boolean isBranch() {
return branch;
}
/**
* Returns a revision with the same timestamp, counter and clusterId as this
* revision and the branch flag set to <code>true</code>.
*
* @return branch revision with this timestamp, counter and clusterId.
*/
public Revision asBranchRevision() {
if (isBranch()) {
return this;
} else {
return new Revision(timestamp, counter, clusterId, true);
}
}
/**
* Returns a revision with the same timestamp, counter and clusterId as this
* revision and the branch flag set to <code>false</code>.
*
* @return trunk revision with this timestamp, counter and clusterId.
*/
public Revision asTrunkRevision() {
if (!isBranch()) {
return this;
} else {
return new Revision(timestamp, counter, clusterId);
}
}
@Override
public int hashCode() {
return (int) (timestamp >>> 32) ^ (int) timestamp ^ counter ^ clusterId;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (other == null) {
return false;
} else if (other.getClass() != this.getClass()) {
return false;
}
Revision r = (Revision) other;
return r.timestamp == this.timestamp &&
r.counter == this.counter &&
r.clusterId == this.clusterId &&
r.branch == this.branch;
}
public int getClusterId() {
return clusterId;
}
@Override
public int getMemory() {
return SHALLOW_MEMORY_USAGE;
}
private static void toHexString(StringBuilder sb, long x) {
int bitCount = (64 - Long.numberOfLeadingZeros(x));
bitCount = Math.max(0, ((bitCount + 3) / 4 * 4) - 4);
for (int i = bitCount; i >= 0; i -= 4) {
int t = (int) (x >> i) & 15;
if (t > 9) {
t = t - 10 + 'a';
} else {
t += '0';
}
sb.append((char) t);
}
}
}