blob: 10e77c780c83f217dcc25b22e32d2ee87d3ae490 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.todos.persistence.syncbase;
import java.util.UUID;
public class IdGenerator {
// TODO(rosswang): Escape collection IDs to allow any character. A lot of this logic can be
// simplified if we can use any character.
// TODO(rosswang): Put this into Syncbase utils.
private final IdAlphabet mAlphabet;
/**
* the length we *would* need if we could use an underscore for the first character
*/
private final int mLongUniformRadixLength;
private final int mLongEncodedLength;
/**
* This is related to unsigned division.
* @see #transformToSignedDivision(long)
*/
private final int mMaxSafeMostSignificantDigit;
/**
* This is related to unsigned division.
* @see #transformToSignedDivision(long)
*/
private final long mMostSignificantUnit, mMaxSafeMostSignificantValue;
private static final double LOG_LONG = Long.SIZE * Math.log(2);
/**
* @param alphabet the alphabet of legal characters for IDs generated by this generator
* @param perturb whether or not to perturb generated IDs by a random perturbation to avoid
* collisions in distributed key generation. Pass {@code false} if this generator
* is used to generate a sort key that is a secondary key where ordering
* conflicts can be deterministically resolved by the primary key. This setting
* only applies to the {@code generate...} methods.
*/
public IdGenerator(IdAlphabet alphabet, boolean perturb) {
mAlphabet = alphabet;
mPerturb = perturb;
// ceil(log_RADIX(Long.MAX + 1))
mLongUniformRadixLength = (int)Math.ceil(LOG_LONG / Math.log(alphabet.radix()));
// int pow, msu = radix ^ url
long mostSignificantUnit = alphabet.radix();
for (int i = 2; i < mLongUniformRadixLength; i++) {
mostSignificantUnit *= alphabet.radix();
}
mMostSignificantUnit = mostSignificantUnit;
mMaxSafeMostSignificantDigit = (int)(Long.MAX_VALUE / mMostSignificantUnit);
mMaxSafeMostSignificantValue = mMaxSafeMostSignificantDigit * mMostSignificantUnit;
// The first character may have additional restrictions, so we have less chars to work with
// for the first digit. If we still have enough digits, we'll proceed and just use a special
// mapping for the leading digit. Otherwise, we'll pad by a leading 0, thus making the
// remaining encoding the common case.
// So, what most-significant digit do we need to encode the max unsigned value?
PartialDivisionResult div = transformToSignedDivision(-1);
int maxMostSignfiicantDigit = div.partialMostSignificantDigit +
(int)(div.partialRemainder / mMostSignificantUnit);
mLongEncodedLength = maxMostSignfiicantDigit < alphabet.leadingRadix() ?
mLongUniformRadixLength : mLongUniformRadixLength + 1;
}
private static class PartialDivisionResult {
public int partialMostSignificantDigit;
public long partialRemainder;
public PartialDivisionResult(int partialMostSignificantDigit, long partialRemainder) {
this.partialMostSignificantDigit = partialMostSignificantDigit;
this.partialRemainder = partialRemainder;
}
}
/**
* Performs partial calculation of the most significant digit for the alphabet-radix
* representation of {@code unsigned}. To achieve unsigned division, we first assume the
* most significant digit is at least as large as the max safe most significant digit for signed
* division, add one more if neccessary to push the "remainder" positive, and then do normal
* division for the rest.
*/
private PartialDivisionResult transformToSignedDivision(long unsigned) {
if (unsigned < 0) {
long naiveRemainder = unsigned - mMaxSafeMostSignificantValue;
return naiveRemainder < 0 ?
// need one more
new PartialDivisionResult(mMaxSafeMostSignificantDigit + 1,
naiveRemainder - mMostSignificantUnit) :
new PartialDivisionResult(mMaxSafeMostSignificantDigit, naiveRemainder);
} else {
// Already positive; handle with normal arithmetic.
return new PartialDivisionResult(0, unsigned);
}
}
/**
* Converts a signed {@code long} to a {@code String} consistent with the pattern
* {@code [0-9A-Za-z][0-9A-Za-z_]*}, required by Syncbase for
* {@link io.v.v23.services.syncbase.Id} names, with lexicographic ordering consistent with the
* numeric ordering.
* <p>
* Example transformations using {@link IdAlphabets#COLLECTION_ID}:
* <pre>
* -9223372036854775808 -> 00000000000
* -9223372036854775807 -> 00000000001
* -9223372036854775806 -> 00000000002
* -1 -> 9MxfBQuKjH7
* 0 -> 9MxfBQuKjH8
* 1 -> 9MxfBQuKjH9
* 9223372036854775805 -> IivLMqoeTYD
* 9223372036854775806 -> IivLMqoeTYE
* 9223372036854775807 -> IivLMqoeTYF
* </pre>
*/
public String longToPaddedIdentifier(long x) {
StringBuilder s = new StringBuilder(mLongEncodedLength);
long unsignedWithSameOrdering = x - Long.MIN_VALUE;
PartialDivisionResult signedSubproblem =
transformToSignedDivision(unsignedWithSameOrdering);
long leftToEncode = signedSubproblem.partialRemainder;
for (int placeValue = 0; placeValue < mLongUniformRadixLength - 1; placeValue++) {
if (leftToEncode > 0) {
s.append(mAlphabet.encodeDigit((int) (leftToEncode % mAlphabet.radix())));
leftToEncode /= mAlphabet.radix();
} else {
s.append(mAlphabet.encodeDigit(0));
}
}
// The most-significant digit needs to be handled specially, accounting for the signed
// division adjustment and using a possibly different character mapping.
int mostSignificantDigit = signedSubproblem.partialMostSignificantDigit + (int)leftToEncode;
if (mLongEncodedLength > mLongUniformRadixLength) {
// just 0-pad
s.append(mAlphabet.encodeDigit(mostSignificantDigit))
.append(mAlphabet.encodeLeadingDigit(0));
} else {
// use the special leading-digit mapping
s.append(mAlphabet.encodeLeadingDigit(mostSignificantDigit));
}
return s.reverse().toString();
}
/**
* Perturbs IDs to mitigate collision during distributed, eventually consistent insertion.
* {@link UUID#randomUUID()} is used for perturbation. The perturbed ID is strictly greater than
* the base ID but less than the next increment from the base ID except in the degenerate case
* where the base ID must be extended to increment.
*/
public String perturb(String baseId) {
UUID uuid = UUID.randomUUID();
return baseId +
longToPaddedIdentifier(uuid.getMostSignificantBits()) +
longToPaddedIdentifier(uuid.getLeastSignificantBits());
}
/**
* Increments an ID with a proposed base length. If the increment cannot be done under these
* constraints, the ID is extended. The incremented ID is guaranteed to be greater than the
* given {@code fullId}.
* <p>
* Example transformations:
* <pre>
* 0000 -> 01
* AZ00 -> A_
* Az00 -> B0
* Zz00 -> a0
* zz00 -> zz1
* zzz0 -> zzz1
* zzzz -> zzzz0
* </pre>
*/
public String increment(String fullId, int proposedCutoff) {
StringBuilder mutable = new StringBuilder(fullId);
if (proposedCutoff > fullId.length()) {
proposedCutoff = fullId.length();
}
// First try normal incrementing
for (int i = proposedCutoff - 1; i > 0; i--) {
int newValue = mAlphabet.decodeDigit(mutable.charAt(i)) + 1;
if (newValue < mAlphabet.radix()) {
mutable.setCharAt(i, mAlphabet.encodeDigit(newValue));
mutable.setLength(proposedCutoff);
return mutable.toString();
} else {
mutable.setCharAt(i, mAlphabet.encodeDigit(0));
}
}
// Process the leading digit specially
if (proposedCutoff > 0){
int newValue = mAlphabet.decodeLeadingDigit(mutable.charAt(0)) + 1;
if (newValue < mAlphabet.leadingRadix()) {
mutable.setCharAt(0, mAlphabet.encodeLeadingDigit(newValue));
mutable.setLength(proposedCutoff);
return mutable.toString();
}
}
// If we've gotten to this point, we can't increment the prefix; start looking at the
// suffix.
// Reset mutable
mutable.setLength(0);
mutable.append(fullId);
for (int i = proposedCutoff; i < fullId.length(); i++) {
int newValue = mAlphabet.decodeDigit(mutable.charAt(i)) + 1;
if (newValue < mAlphabet.radix()) {
mutable.setCharAt(i, mAlphabet.encodeDigit(newValue));
mutable.setLength(i + 1);
return mutable.toString();
}
}
// By this point, we just have to extend the string.
mutable.append(mAlphabet.encodeDigit(0));
return mutable.toString();
}
public String increment(String previous) {
return increment(previous, mLongEncodedLength);
}
private final Object mLargestKnownIdMutex = new Object();
private String mLargestKnownId;
private final boolean mPerturb;
public String generateTailId() {
String candidate = longToPaddedIdentifier(System.currentTimeMillis());
synchronized (mLargestKnownIdMutex) {
if (mLargestKnownId != null && candidate.compareTo(mLargestKnownId) <= 0) {
candidate = increment(mLargestKnownId);
}
return mLargestKnownId = mPerturb? perturb(candidate) : candidate;
}
}
public void registerId(String id) {
synchronized (mLargestKnownIdMutex) {
if (mLargestKnownId == null || id.compareTo(mLargestKnownId) > 0) {
mLargestKnownId = id;
}
}
}
}