io.hypersistence.tsid.TSID Maven / Gradle / Ivy
Show all versions of hypersistence-tsid Show documentation
/*
* MIT License
*
* Copyright (c) 2020-2022 Fabio Lima
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.hypersistence.tsid;
import java.io.Serializable;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.Instant;
import java.util.Random;
import java.util.SplittableRandom;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntFunction;
import java.util.function.IntSupplier;
/**
* A value object that represents a Time-Sorted Unique Identifier (TSID).
*
* TSID is a 64-bit value that has 2 components:
*
* - Time component (42 bits): a number of milliseconds since
* 1970-01-01 (Unix epoch).
*
- Random component (22 bits): a sequence of random bits generated by
* a secure random generator.
*
*
* The Random component has 2 sub-parts:
*
* - Node (0 to 20 bits): a number used to identify the machine or
* node.
*
- Counter (2 to 22 bits): a randomly generated number that is
* incremented whenever the time component is repeated.
*
*
* The random component layout depend on the node bits. If the node bits are 10,
* the counter bits are limited to 12. In this example, the maximum node value
* is 2^10-1 = 1023 and the maximum counter value is 2^12-1 = 4093. So the
* maximum TSIDs that can be generated per millisecond per node is 4096.
*
* Instances of this class are immutable.
*
* @see Snowflake ID
*/
public final class TSID implements Serializable, Comparable {
private static final long serialVersionUID = -5446820982139116297L;
private final long number;
/**
* Number of bytes of a TSID.
*/
public static final int TSID_BYTES = 8;
/**
* Number of characters of a TSID.
*/
public static final int TSID_CHARS = 13;
/**
* Number of milliseconds of 2020-01-01T00:00:00.000Z.
*/
public static final long TSID_EPOCH = Instant.parse("2020-01-01T00:00:00.000Z").toEpochMilli();
static final int RANDOM_BITS = 22;
static final int RANDOM_MASK = 0x003fffff;
private static final char[] ALPHABET_UPPERCASE = //
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', //
'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z' };
private static final char[] ALPHABET_LOWERCASE = //
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', //
'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z' };
private static final long[] ALPHABET_VALUES = new long[128];
static {
for (int i = 0; i < ALPHABET_VALUES.length; i++) {
ALPHABET_VALUES[i] = -1;
}
// Numbers
ALPHABET_VALUES['0'] = 0x00;
ALPHABET_VALUES['1'] = 0x01;
ALPHABET_VALUES['2'] = 0x02;
ALPHABET_VALUES['3'] = 0x03;
ALPHABET_VALUES['4'] = 0x04;
ALPHABET_VALUES['5'] = 0x05;
ALPHABET_VALUES['6'] = 0x06;
ALPHABET_VALUES['7'] = 0x07;
ALPHABET_VALUES['8'] = 0x08;
ALPHABET_VALUES['9'] = 0x09;
// Lower case
ALPHABET_VALUES['a'] = 0x0a;
ALPHABET_VALUES['b'] = 0x0b;
ALPHABET_VALUES['c'] = 0x0c;
ALPHABET_VALUES['d'] = 0x0d;
ALPHABET_VALUES['e'] = 0x0e;
ALPHABET_VALUES['f'] = 0x0f;
ALPHABET_VALUES['g'] = 0x10;
ALPHABET_VALUES['h'] = 0x11;
ALPHABET_VALUES['j'] = 0x12;
ALPHABET_VALUES['k'] = 0x13;
ALPHABET_VALUES['m'] = 0x14;
ALPHABET_VALUES['n'] = 0x15;
ALPHABET_VALUES['p'] = 0x16;
ALPHABET_VALUES['q'] = 0x17;
ALPHABET_VALUES['r'] = 0x18;
ALPHABET_VALUES['s'] = 0x19;
ALPHABET_VALUES['t'] = 0x1a;
ALPHABET_VALUES['v'] = 0x1b;
ALPHABET_VALUES['w'] = 0x1c;
ALPHABET_VALUES['x'] = 0x1d;
ALPHABET_VALUES['y'] = 0x1e;
ALPHABET_VALUES['z'] = 0x1f;
// Lower case OIL
ALPHABET_VALUES['o'] = 0x00;
ALPHABET_VALUES['i'] = 0x01;
ALPHABET_VALUES['l'] = 0x01;
// Upper case
ALPHABET_VALUES['A'] = 0x0a;
ALPHABET_VALUES['B'] = 0x0b;
ALPHABET_VALUES['C'] = 0x0c;
ALPHABET_VALUES['D'] = 0x0d;
ALPHABET_VALUES['E'] = 0x0e;
ALPHABET_VALUES['F'] = 0x0f;
ALPHABET_VALUES['G'] = 0x10;
ALPHABET_VALUES['H'] = 0x11;
ALPHABET_VALUES['J'] = 0x12;
ALPHABET_VALUES['K'] = 0x13;
ALPHABET_VALUES['M'] = 0x14;
ALPHABET_VALUES['N'] = 0x15;
ALPHABET_VALUES['P'] = 0x16;
ALPHABET_VALUES['Q'] = 0x17;
ALPHABET_VALUES['R'] = 0x18;
ALPHABET_VALUES['S'] = 0x19;
ALPHABET_VALUES['T'] = 0x1a;
ALPHABET_VALUES['V'] = 0x1b;
ALPHABET_VALUES['W'] = 0x1c;
ALPHABET_VALUES['X'] = 0x1d;
ALPHABET_VALUES['Y'] = 0x1e;
ALPHABET_VALUES['Z'] = 0x1f;
// Upper case OIL
ALPHABET_VALUES['O'] = 0x00;
ALPHABET_VALUES['I'] = 0x01;
ALPHABET_VALUES['L'] = 0x01;
}
/**
* Creates a new TSID.
*
* This constructor wraps the input value in an immutable object.
*
* @param number a number
*/
public TSID(final long number) {
this.number = number;
}
/**
* Converts a number into a TSID.
*
* This method wraps the input value in an immutable object.
*
* @param number a number
* @return a TSID
*/
public static TSID from(final long number) {
return new TSID(number);
}
/**
* Converts a byte array into a TSID.
*
* @param bytes a byte array
* @return a TSID
* @throws IllegalArgumentException if bytes are null or its length is not 8
*/
public static TSID from(final byte[] bytes) {
if (bytes == null || bytes.length != TSID_BYTES) {
throw new IllegalArgumentException("Invalid TSID bytes"); // null or wrong length!
}
long number = 0;
number |= (bytes[0x0] & 0xffL) << 56;
number |= (bytes[0x1] & 0xffL) << 48;
number |= (bytes[0x2] & 0xffL) << 40;
number |= (bytes[0x3] & 0xffL) << 32;
number |= (bytes[0x4] & 0xffL) << 24;
number |= (bytes[0x5] & 0xffL) << 16;
number |= (bytes[0x6] & 0xffL) << 8;
number |= (bytes[0x7] & 0xffL);
return new TSID(number);
}
/**
* Converts a canonical string into a TSID.
*
* The input string must be 13 characters long and must contain only characters
* from Crockford's base 32 alphabet.
*
* The first character of the input string must be between 0 and F.
*
* @param string a canonical string
* @return a TSID
* @throws IllegalArgumentException if the input string is invalid
* @see Crockford's Base 32
*/
public static TSID from(final String string) {
final char[] chars = toCharArray(string);
long number = 0;
number |= ALPHABET_VALUES[chars[0x00]] << 60;
number |= ALPHABET_VALUES[chars[0x01]] << 55;
number |= ALPHABET_VALUES[chars[0x02]] << 50;
number |= ALPHABET_VALUES[chars[0x03]] << 45;
number |= ALPHABET_VALUES[chars[0x04]] << 40;
number |= ALPHABET_VALUES[chars[0x05]] << 35;
number |= ALPHABET_VALUES[chars[0x06]] << 30;
number |= ALPHABET_VALUES[chars[0x07]] << 25;
number |= ALPHABET_VALUES[chars[0x08]] << 20;
number |= ALPHABET_VALUES[chars[0x09]] << 15;
number |= ALPHABET_VALUES[chars[0x0a]] << 10;
number |= ALPHABET_VALUES[chars[0x0b]] << 5;
number |= ALPHABET_VALUES[chars[0x0c]];
return new TSID(number);
}
/**
* Converts the TSID into a number.
*
* This method simply unwraps the internal value.
*
* @return an number.
*/
public long toLong() {
return this.number;
}
/**
* Converts the TSID into a byte array.
*
* @return an byte array.
*/
public byte[] toBytes() {
final byte[] bytes = new byte[TSID_BYTES];
bytes[0x0] = (byte) (number >>> 56);
bytes[0x1] = (byte) (number >>> 48);
bytes[0x2] = (byte) (number >>> 40);
bytes[0x3] = (byte) (number >>> 32);
bytes[0x4] = (byte) (number >>> 24);
bytes[0x5] = (byte) (number >>> 16);
bytes[0x6] = (byte) (number >>> 8);
bytes[0x7] = (byte) (number);
return bytes;
}
/**
* Returns a fast new TSID.
*
* This static method is a quick alternative to {@link Factory#getTsid()}.
*
* It employs {@link AtomicInteger} to generate up to 2^22 (4,194,304) TSIDs per
* millisecond. It can be useful, for example, for logging.
*
* Security-sensitive applications that require a cryptographically secure
* pseudo-random generator should use {@link Factory#getTsid()}.
*
* System property "tsidcreator.node" and environment variable
* "TSID_NODE" are ignored by this method. Therefore, there will be
* collisions if more than one process is generating TSIDs using this method. In
* that case, {@link Factory#getTsid()} should be used in conjunction
* with that property or variable.
*
* @return a TSID
* @see {@link AtomicInteger}
* @since 5.1.0
*/
public static TSID fast() {
final long time = (System.currentTimeMillis() - TSID_EPOCH) << RANDOM_BITS;
final long tail = LazyHolder.counter.incrementAndGet() & RANDOM_MASK;
return new TSID(time | tail);
}
/**
* Converts the TSID into a canonical string in upper case.
*
* The output string is 13 characters long and contains only characters from
* Crockford's base 32 alphabet.
*
* For lower case string, use the shorthand {@code Tsid.toLowerCase()} instead
* of {@code Tsid.toString().toLowerCase()}.
*
* @return a TSID string
* @see Crockford's Base 32
*/
@Override
public String toString() {
return toString(ALPHABET_UPPERCASE);
}
/**
* Converts the TSID into a canonical string in lower case.
*
* The output string is 13 characters long and contains only characters from
* Crockford's base 32 alphabet.
*
* It is faster shorthand for {@code Tsid.toString().toLowerCase()}.
*
* @return a string
* @see Crockford's Base 32
*/
public String toLowerCase() {
return toString(ALPHABET_LOWERCASE);
}
/**
* Returns the instant of creation.
*
* The instant of creation is extracted from the time component.
*
* @return {@link Instant}
*/
public Instant getInstant() {
return Instant.ofEpochMilli(getUnixMilliseconds());
}
/**
* Returns the instant of creation.
*
* The instant of creation is extracted from the time component.
*
* @param customEpoch the custom epoch instant
* @return {@link Instant}
*/
public Instant getInstant(final Instant customEpoch) {
return Instant.ofEpochMilli(getUnixMilliseconds(customEpoch.toEpochMilli()));
}
/**
* Returns the time of creation in milliseconds since 1970-01-01.
*
* The time of creation is extracted from the time component.
*
* @return the number of milliseconds since 1970-01-01
*/
public long getUnixMilliseconds() {
return this.getTime() + TSID_EPOCH;
}
/**
* Returns the time of creation in milliseconds since 1970-01-01.
*
* The time of creation is extracted from the time component.
*
* @param customEpoch the custom epoch in milliseconds since 1970-01-01
* @return the number of milliseconds since 1970-01-01
*/
public long getUnixMilliseconds(final long customEpoch) {
return this.getTime() + customEpoch;
}
/**
* Returns the time component as a number.
*
* The time component is a number between 0 and 2^42-1.
*
* @return a number of milliseconds.
*/
long getTime() {
return this.number >>> RANDOM_BITS;
}
/**
* Returns the random component as a number.
*
* The time component is a number between 0 and 2^22-1.
*
* @return a number
*/
long getRandom() {
return this.number & RANDOM_MASK;
}
/**
* Checks if the input string is valid.
*
* The input string must be 13 characters long and must contain only characters
* from Crockford's base 32 alphabet.
*
* The first character of the input string must be between 0 and F.
*
* @param string a string
* @return true if valid
*/
public static boolean isValid(final String string) {
return string != null && isValidCharArray(string.toCharArray());
}
/**
* Returns a hash code value for the TSID.
*/
@Override
public int hashCode() {
return (int) (number ^ (number >>> 32));
}
/**
* Checks if some other TSID is equal to this one.
*/
@Override
public boolean equals(Object other) {
if (other == null)
return false;
if (other.getClass() != TSID.class)
return false;
TSID that = (TSID) other;
return (this.number == that.number);
}
/**
* Compares two TSIDs as unsigned 64-bit integers.
*
* The first of two TSIDs is greater than the second if the most significant
* byte in which they differ is greater for the first TSID.
*
* @param that a TSID to be compared with
* @return -1, 0 or 1 as {@code this} is less than, equal to, or greater than
* {@code that}
*/
@Override
public int compareTo(TSID that) {
// used to compare as UNSIGNED longs
final long min = 0x8000000000000000L;
final long a = this.number + min;
final long b = that.number + min;
if (a > b)
return 1;
else if (a < b)
return -1;
return 0;
}
/**
* Converts the TSID to a base-n encoded string.
*
* Example:
*
* - TSID: 0AXS751X00W7C
*
- Base: 62
*
- Output: 0T5jFDIkmmy
*
*
* The output string is left padded with zeros.
*
* @param base a radix between 2 and 62
* @return a base-n encoded string
* @since 5.2.0
*/
public String encode(final int base) {
if (base < 2 || base > 62) {
throw new IllegalArgumentException(String.format("Invalid base: %s", base));
}
return BaseN.encode(this, base);
}
/**
* Converts a base-n encoded string to a TSID.
*
* Example:
*
* - String: 05772439BB9F9074
*
- Base: 16
*
- Output: 0AXS476XSZ43M
*
*
* The input string is left padded with zeros.
*
* @param string a base-n encoded string
* @param base a radix between 2 and 62
* @return a TSID
* @since 5.2.0
*/
public static TSID decode(final String string, final int base) {
if (base < 2 || base > 62) {
throw new IllegalArgumentException(String.format("Invalid base: %s", base));
}
return BaseN.decode(string, base);
}
/**
* Converts the TSID to a string using a custom format.
*
* The custom format uses a placeholder that will be substituted by the TSID
* string. Only the first occurrence of a placeholder will replaced.
*
* Placeholders:
*
* - %S: canonical string in upper case
*
- %s: canonical string in lower case
*
- %X: hexadecimal in upper case
*
- %x: hexadecimal in lower case
*
- %d: base-10
*
- %z: base-62
*
*
* Examples:
*
* - An key that starts with a letter:
*
* - TSID: 0AWE5HZP3SKTK
*
- Format: K%S
*
- Output: K0AWE5HZP3SKTK
*
*
* - A file name in hexadecimal with a prefix and an extension:
*
* - TSID: 0AXFXR5W7VBX0
*
- Format: DOC-%X.PDF
*
- Output: DOC-0575FDC1786137D6.PDF
*
*
*
*
* The opposite operation can be done by {@link TSID#unformat(String, String)}.
*
* @param format a custom format
* @return a string using a custom format
* @since 5.2.0
*/
public String format(final String format) {
if (format != null) {
final int i = format.indexOf("%");
if (i < 0 || i == format.length() - 1) {
throw new IllegalArgumentException(String.format("Invalid format string: \"%s\"", format));
}
final String head = format.substring(0, i);
final String tail = format.substring(i + 2);
final char placeholder = format.charAt(i + 1);
switch (placeholder) {
case 'S': // canonical string in upper case
return head + toString() + tail;
case 's': // canonical string in lower case
return head + toLowerCase() + tail;
case 'X': // hexadecimal in upper case
return head + BaseN.encode(this, 16) + tail;
case 'x': // hexadecimal in lower case
return head + BaseN.encode(this, 16).toLowerCase() + tail;
case 'd': // base-10
return head + BaseN.encode(this, 10) + tail;
case 'z': // base-62
return head + BaseN.encode(this, 62) + tail;
default:
throw new IllegalArgumentException(String.format("Invalid placeholder: \"%%%s\"", placeholder));
}
}
throw new IllegalArgumentException(String.format("Invalid format string: \"%s\"", format));
}
/**
* Converts a string using a custom format to a TSID.
*
* This method does the opposite operation of {@link TSID#format(String)}.
*
* Examples:
*
* - An key that starts with a letter:
*
* - String: K0AWE5HZP3SKTK
*
- Format: K%S
*
- Output: 0AWE5HZP3SKTK
*
*
* - A file name in hexadecimal with a prefix and an extension:
*
* - String: DOC-0575FDC1786137D6.PDF
*
- Format: DOC-%X.PDF
*
- Output: 0AXFXR5W7VBX0
*
*
*
*
* @param formatted a string using a custom format
* @param format a custom format
* @return a TSID
* @since 5.2.0
*/
public static TSID unformat(final String formatted, final String format) {
if (formatted != null && format != null) {
final int i = format.indexOf("%");
if (i < 0 || i == format.length() - 1) {
throw new IllegalArgumentException(String.format("Invalid format string: \"%s\"", format));
}
final String head = format.substring(0, i);
final String tail = format.substring(i + 2);
final char placeholder = format.charAt(i + 1);
final int length = formatted.length() - head.length() - tail.length();
if (formatted.startsWith(head) && formatted.endsWith(tail)) {
switch (placeholder) {
case 'S': // canonical string (case insensitive)
return TSID.from(formatted.substring(i, i + length));
case 's': // canonical string (case insensitive)
return TSID.from(formatted.substring(i, i + length));
case 'X': // hexadecimal (case insensitive)
return BaseN.decode(formatted.substring(i, i + length).toUpperCase(), 16);
case 'x': // hexadecimal (case insensitive)
return BaseN.decode(formatted.substring(i, i + length).toUpperCase(), 16);
case 'd': // base-10
return BaseN.decode(formatted.substring(i, i + length), 10);
case 'z': // base-62
return BaseN.decode(formatted.substring(i, i + length), 62);
default:
throw new IllegalArgumentException(String.format("Invalid placeholder: \"%%%s\"", placeholder));
}
}
}
throw new IllegalArgumentException(String.format("Invalid formatted string: \"%s\"", formatted));
}
String toString(final char[] alphabet) {
final char[] chars = new char[TSID_CHARS];
chars[0x00] = alphabet[(int) ((number >>> 60) & 0b11111)];
chars[0x01] = alphabet[(int) ((number >>> 55) & 0b11111)];
chars[0x02] = alphabet[(int) ((number >>> 50) & 0b11111)];
chars[0x03] = alphabet[(int) ((number >>> 45) & 0b11111)];
chars[0x04] = alphabet[(int) ((number >>> 40) & 0b11111)];
chars[0x05] = alphabet[(int) ((number >>> 35) & 0b11111)];
chars[0x06] = alphabet[(int) ((number >>> 30) & 0b11111)];
chars[0x07] = alphabet[(int) ((number >>> 25) & 0b11111)];
chars[0x08] = alphabet[(int) ((number >>> 20) & 0b11111)];
chars[0x09] = alphabet[(int) ((number >>> 15) & 0b11111)];
chars[0x0a] = alphabet[(int) ((number >>> 10) & 0b11111)];
chars[0x0b] = alphabet[(int) ((number >>> 5) & 0b11111)];
chars[0x0c] = alphabet[(int) (number & 0b11111)];
return new String(chars);
}
static char[] toCharArray(final String string) {
char[] chars = string == null ? null : string.toCharArray();
if (!isValidCharArray(chars)) {
throw new IllegalArgumentException(String.format("Invalid TSID string: \"%s\"", string));
}
return chars;
}
/**
* Checks if the string is a valid TSID.
*
* A valid TSID string is a sequence of 13 characters from Crockford's base 32
* alphabet.
*
* The first character of the input string must be between 0 and F.
*
* @param chars a char array
* @return boolean true if valid
*/
static boolean isValidCharArray(final char[] chars) {
if (chars == null || chars.length != TSID_CHARS) {
return false; // null or wrong size!
}
// The extra bit added by base-32 encoding must be zero
// As a consequence, the 1st char of the input string must be between 0 and F.
if ((ALPHABET_VALUES[chars[0]] & 0b10000) != 0) {
return false; // overflow!
}
for (int i = 0; i < chars.length; i++) {
if (ALPHABET_VALUES[chars[i]] == -1) {
return false; // invalid character!
}
}
return true; // It seems to be OK.
}
static class BaseN {
private static final BigInteger MAX = BigInteger.valueOf(2).pow(64);
private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // base-62
static String encode(final TSID tsid, final int base) {
BigInteger x = new BigInteger(1, tsid.toBytes());
final BigInteger radix = BigInteger.valueOf(base);
final int length = (int) Math.ceil(Long.SIZE / (Math.log(base) / Math.log(2)));
int b = length; // buffer index
char[] buffer = new char[length];
while (x.compareTo(BigInteger.ZERO) > 0) {
BigInteger[] result = x.divideAndRemainder(radix);
buffer[--b] = ALPHABET.charAt(result[1].intValue());
x = result[0];
}
while (b > 0) {
buffer[--b] = '0';
}
return new String(buffer);
}
static TSID decode(final String string, final int base) {
BigInteger x = BigInteger.ZERO;
final BigInteger radix = BigInteger.valueOf(base);
final int length = (int) Math.ceil(Long.SIZE / (Math.log(base) / Math.log(2)));
if (string == null) {
throw new IllegalArgumentException(String.format("Invalid base-%d string: null", base));
}
if (string.length() != length) {
throw new IllegalArgumentException(String.format("Invalid base-%d length: %s", base, string.length()));
}
for (int i = 0; i < string.length(); i++) {
final long plus = (int) ALPHABET.indexOf(string.charAt(i));
if (plus < 0 || plus >= base) {
throw new IllegalArgumentException(
String.format("Invalid base-%d character: %s", base, string.charAt(i)));
}
x = x.multiply(radix).add(BigInteger.valueOf(plus));
}
if (x.compareTo(MAX) > 0) {
throw new IllegalArgumentException(String.format("Invalid base-%d value (overflow): %s", base, x));
}
return new TSID(x.longValue());
}
}
private static class LazyHolder {
private static final AtomicInteger counter = new AtomicInteger((new SplittableRandom()).nextInt());
}
/**
* A factory that actually generates Time-Sorted Unique Identifiers (TSID).
*
* You can use this class to generate a Tsid or to make some customizations,
* for example changing the default {@link SecureRandom} random generator to a faster pseudo-random generator.
*
* If a system property "tsidcreator.node" or environment variable
* "TSID_NODE" is defined, its value is utilized as node identifier. One
* of them should be defined to embed a machine ID in the generated TSID
* in order to avoid TSID collisions. Using that property or variable is
* highly recommended. If no property or variable is defined, a random
* node ID is generated at initialization.
*
* If a system property "tsidcreator.node.count" or environment variable
* "TSID_NODE_COUNT" is defined, its value is utilized by the
* constructors of this class to adjust the amount of bits needed to embed the
* node ID. For example, if the number 50 is given, the node bit amount is
* adjusted to 6, which is the minimum number of bits to accommodate 50 nodes.
* If no property or variable is defined, the number of bits reserved for node
* ID is set to 10, which can accommodate 1024 nodes.
*
* This class should be used as a singleton. Make sure that you create
* and reuse a single instance of {@link Factory} per node in your
* distributed system.
*/
public static final class Factory {
private static final ReentrantLock LOCK = new ReentrantLock();
public static final Factory INSTANCE = new Factory();
public static final Factory INSTANCE_256 = newInstance256();
public static final Factory INSTANCE_1024 = newInstance1024();
public static final Factory INSTANCE_4096 = newInstance4096();
public static final IntSupplier THREAD_LOCAL_RANDOM_FUNCTION = () -> ThreadLocalRandom.current().nextInt();
private int counter;
private long lastTime;
private final int node;
private final int nodeBits;
private final int counterBits;
private final int nodeMask;
private final int counterMask;
private final Clock clock;
private final long customEpoch;
private final IRandom random;
private final int randomBytes;
static final int NODE_BITS_256 = 8;
static final int NODE_BITS_1024 = 10;
static final int NODE_BITS_4096 = 12;
// ******************************
// Constructors
// ******************************
/**
* It builds a new factory.
*
* The node identifier provided by the "tsidcreator.node" system property or the
* "TSID_NODE" environment variable is embedded in the generated TSIDs in
* order to avoid collisions. It is highly recommended defining that
* property or variable. Otherwise the node identifier will be randomly chosen.
*
* If a system property "tsidcreator.node.count" or environment variable
* "TSID_NODE_COUNT" is defined, its value is used to adjust the node
* bits amount.
*/
public Factory() {
this(builder());
}
/**
* It builds a new factory.
*
* The node identifier provided by parameter is embedded in the generated TSIDs
* in order to avoid collisions.
*
* If a system property "tsidcreator.node.count" or environment variable
* "TSID_NODE_COUNT" is defined, its value is used to adjust the node
* bits amount.
*
* @param node the node identifier
*/
public Factory(int node) {
this(builder().withNode(node));
}
/**
* It builds a generator with the given builder.
*
* @param builder a builder instance
*/
private Factory(Builder builder) {
// setup node bits, custom epoch and random function
this.customEpoch = builder.getCustomEpoch();
this.nodeBits = builder.getNodeBits();
this.random = builder.getRandom();
this.clock = builder.getClock();
// setup constants that depend on node bits
this.counterBits = RANDOM_BITS - nodeBits;
this.counterMask = RANDOM_MASK >>> nodeBits;
this.nodeMask = RANDOM_MASK >>> counterBits;
// setup how many bytes to get from the random function
this.randomBytes = ((this.counterBits - 1) / 8) + 1;
// setup the node identifier
this.node = builder.getNode() & nodeMask;
// finally, initialize internal state
this.lastTime = clock.millis();
try {
LOCK.lock();
this.counter = getRandomValue();
} finally {
LOCK.unlock();
}
}
/**
* Returns a new factory for up to 256 nodes and 16384 ID/ms.
*
* @return {@link Factory}
*/
public static Factory newInstance256() {
return Factory.builder().withNodeBits(NODE_BITS_256).build();
}
/**
* Returns a new factory for up to 256 nodes and 16384 ID/ms.
*
* @param node the node identifier
* @return {@link Factory}
*/
public static Factory newInstance256(int node) {
return Factory.builder().withNodeBits(NODE_BITS_256).withNode(node).build();
}
/**
* Returns a new factory for up to 1024 nodes and 4096 ID/ms.
*
* It is equivalent to {@code new TsidFactory()}.
*
* @return {@link Factory}
*/
public static Factory newInstance1024() {
return Factory.builder().withNodeBits(NODE_BITS_1024).build();
}
/**
* Returns a new factory for up to 1024 nodes and 4096 ID/ms.
*
* It is equivalent to {@code new TsidFactory(int)}.
*
* @param node the node identifier
* @return {@link Factory}
*/
public static Factory newInstance1024(int node) {
return Factory.builder().withNodeBits(NODE_BITS_1024).withNode(node).build();
}
/**
* Returns a new factory for up to 4096 nodes and 1024 ID/ms.
*
* @return {@link Factory}
*/
public static Factory newInstance4096() {
return Factory.builder().withNodeBits(NODE_BITS_4096).build();
}
/**
* Returns a new factory for up to 4096 nodes and 1024 ID/ms.
*
* @param node the node identifier
* @return {@link Factory}
*/
public static Factory newInstance4096(int node) {
return Factory.builder().withNodeBits(NODE_BITS_4096).withNode(node).build();
}
// ******************************
// Public methods
// ******************************
/**
* Returns a TSID.
*
* @return a TSID.
*/
public TSID generate() {
final long _time;
final long _counter;
try {
LOCK.lock();
_time = getTime() << RANDOM_BITS;
_counter = (long) this.counter & this.counterMask;
} finally {
LOCK.unlock();
}
final long _node = (long) this.node << this.counterBits;
return new TSID(_time | _node | _counter);
}
/**
* Returns the current time.
*
* If the current time is equal to the previous time, the counter is incremented
* by one. Otherwise, the counter is reset to a random value.
*
* The maximum number of increment operations depend on the counter bits. For
* example, if the counter bits is 12, the maximum number of increment
* operations is 2^12 = 4096.
*
* @return the current time
*/
private long getTime() {
long time = clock.millis();
if (time <= this.lastTime) {
this.counter++;
// Carry is 1 if an overflow occurs after ++.
int carry = this.counter >>> this.counterBits;
this.counter = this.counter & this.counterMask;
time = this.lastTime + carry; // increment time
} else {
// If the system clock has advanced as expected,
// simply reset the counter to a new random value.
this.counter = this.getRandomValue();
}
// save current time
this.lastTime = time;
// adjust to the custom epoch
return time - this.customEpoch;
}
/**
* Returns a random counter value from 0 to 0x3fffff (2^22-1 = 4,194,303).
*
* The counter maximum value depends on the node identifier bits. For example,
* if the node identifier has 10 bits, the counter has 12 bits.
*
* @return a number
*/
private int getRandomCounter() {
if (random instanceof ByteRandom) {
final byte[] bytes = random.nextBytes(this.randomBytes);
switch (bytes.length) {
case 1:
return (bytes[0] & 0xff) & this.counterMask;
case 2:
return (((bytes[0] & 0xff) << 8) | (bytes[1] & 0xff)) & this.counterMask;
default:
return (((bytes[0] & 0xff) << 16) | ((bytes[1] & 0xff) << 8) | (bytes[2] & 0xff)) & this.counterMask;
}
} else {
return random.nextInt() & this.counterMask;
}
}
/**
* Returns a random value based on the counter and the current Thread id.
*
* @return a number
*/
private int getRandomValue() {
int randomCounter = getRandomCounter();
int threadId = (((int) (Thread.currentThread().getId()) % 256) << (counterBits - 8));
return (threadId | (randomCounter >> (counterBits - 8)));
}
/**
* Returns a builder object.
*
* It is used to build a custom {@link Factory}.
*/
public static Builder builder() {
return new Builder();
}
// ******************************
// Package-private inner classes
// ******************************
/**
* A nested class that builds custom TSID factories.
*
* It is used to setup a custom {@link Factory}.
*/
public static class Builder {
private Integer node;
private Integer nodeBits;
private Long customEpoch;
private IRandom random;
private Clock clock;
/**
* Set the node identifier.
*
* @param node a number that must be between 0 and 2^nodeBits-1.
* @return {@link Builder}
* @throws IllegalArgumentException if the node identifier is out of the range
* [0, 2^nodeBits-1] when {@code build()} is
* invoked
*/
public Builder withNode(Integer node) {
this.node = node;
return this;
}
/**
* Set the node identifier bits length.
*
* @param nodeBits a number that must be between 0 and 20.
* @return {@link Builder}
* @throws IllegalArgumentException if the node bits are out of the range [0,
* 20] when {@code build()} is invoked
*/
public Builder withNodeBits(Integer nodeBits) {
this.nodeBits = nodeBits;
return this;
}
/**
* Set the custom epoch.
*
* @param customEpoch an instant that represents the custom epoch.
* @return {@link Builder}
*/
public Builder withCustomEpoch(Instant customEpoch) {
this.customEpoch = customEpoch.toEpochMilli();
return this;
}
/**
* Set the random generator.
*
* The random generator is used to create a random function that is used to
* reset the counter when the millisecond changes.
*
* @param random a {@link Random} generator
* @return {@link Builder}
*/
public Builder withRandom(Random random) {
if (random != null) {
if (random instanceof SecureRandom) {
this.random = new ByteRandom(random);
} else {
this.random = new IntRandom(random);
}
}
return this;
}
/**
* Set the random function.
*
* The random function is used to reset the counter when the millisecond
* changes.
*
* @param randomFunction a random function that returns a integer value
* @return {@link Builder}
*/
public Builder withRandomFunction(IntSupplier randomFunction) {
this.random = new IntRandom(randomFunction);
return this;
}
/**
* Set the random function.
*
* The random function must return a byte array of a given length.
*
* The random function is used to reset the counter when the millisecond
* changes.
*
* Despite its name, the random function MAY return a fixed value, for example,
* if your app requires the counter to be reset to ZERO whenever the millisecond
* changes, like Twitter Snowflakes, this function should return an array filled
* with ZEROS.
*
* @param randomFunction a random function that returns a byte array
* @return {@link Builder}
*/
public Builder withRandomFunction(IntFunction randomFunction) {
this.random = new ByteRandom(randomFunction);
return this;
}
/**
* Set the clock to be used in tests.
*
* @param clock a clock
* @return {@link Builder}
*/
public Builder withClock(Clock clock) {
this.clock = clock;
return this;
}
/**
* Get the node identifier.
*
* @return a number
* @throws IllegalArgumentException if the node is out of range
*/
protected Integer getNode() {
final int max = (1 << nodeBits) - 1;
if (this.node == null) {
if (Settings.getNode() != null) {
// use property or variable
this.node = Settings.getNode();
} else {
// use random node identifier
this.node = this.random.nextInt() & max;
}
}
if (node < 0 || node > max) {
throw new IllegalArgumentException(String.format("Node ID out of range [0, %s]: %s", max, node));
}
return this.node;
}
/**
* Get the node identifier bits length within the range 0 to 20.
*
* @return a number
* @throws IllegalArgumentException if the node bits are out of range
*/
protected Integer getNodeBits() {
if (this.nodeBits == null) {
if (Settings.getNodeCount() != null) {
// use property or variable
this.nodeBits = (int) Math.ceil(Math.log(Settings.getNodeCount()) / Math.log(2));
} else {
// use default bit length: 10 bits
this.nodeBits = Factory.NODE_BITS_1024;
}
}
if (nodeBits < 0 || nodeBits > 20) {
throw new IllegalArgumentException(String.format("Node bits out of range [0, 20]: %s", nodeBits));
}
return this.nodeBits;
}
/**
* Gets the custom epoch.
*
* @return a number
*/
protected Long getCustomEpoch() {
if (this.customEpoch == null) {
this.customEpoch = TSID_EPOCH; // 2020-01-01
}
return this.customEpoch;
}
/**
* Gets the random generator.
*
* @return a random generator
*/
protected IRandom getRandom() {
if (this.random == null) {
this.withRandom(new SecureRandom());
}
return this.random;
}
/**
* Gets the clock to be used in tests.
*
* @return a clock
*/
protected Clock getClock() {
if (this.clock == null) {
this.withClock(Clock.systemUTC());
}
return this.clock;
}
/**
* Returns a build TSID factory.
*
* @return {@link Factory}
* @throws IllegalArgumentException if the node is out of range
* @throws IllegalArgumentException if the node bits are out of range
*/
public Factory build() {
return new Factory(this);
}
}
interface IRandom {
int nextInt();
byte[] nextBytes(int length);
}
static class IntRandom implements IRandom {
private final IntSupplier randomFunction;
public IntRandom() {
this(newRandomFunction(null));
}
public IntRandom(Random random) {
this(newRandomFunction(random));
}
public IntRandom(IntSupplier randomFunction) {
this.randomFunction = randomFunction != null ? randomFunction : newRandomFunction(null);
}
@Override
public int nextInt() {
return randomFunction.getAsInt();
}
@Override
public byte[] nextBytes(int length) {
int shift = 0;
long random = 0;
final byte[] bytes = new byte[length];
for (int i = 0; i < length; i++) {
if (shift < Byte.SIZE) {
shift = Integer.SIZE;
random = randomFunction.getAsInt();
}
shift -= Byte.SIZE; // 56, 48, 40...
bytes[i] = (byte) (random >>> shift);
}
return bytes;
}
protected static IntSupplier newRandomFunction(Random random) {
final Random entropy = random != null ? random : new SecureRandom();
return entropy::nextInt;
}
}
static class ByteRandom implements IRandom {
private final IntFunction randomFunction;
public ByteRandom() {
this(newRandomFunction(null));
}
public ByteRandom(Random random) {
this(newRandomFunction(random));
}
public ByteRandom(IntFunction randomFunction) {
this.randomFunction = randomFunction != null ? randomFunction : newRandomFunction(null);
}
@Override
public int nextInt() {
int number = 0;
byte[] bytes = this.randomFunction.apply(Integer.BYTES);
for (int i = 0; i < Integer.BYTES; i++) {
number = (number << 8) | (bytes[i] & 0xff);
}
return number;
}
@Override
public byte[] nextBytes(int length) {
return this.randomFunction.apply(length);
}
protected static IntFunction newRandomFunction(Random random) {
final Random entropy = random != null ? random : new SecureRandom();
return (final int length) -> {
final byte[] bytes = new byte[length];
entropy.nextBytes(bytes);
return bytes;
};
}
}
static class Settings {
static final String NODE = "tsid.node";
static final String NODE_COUNT = "tsid.node.count";
private Settings() {
}
public static Integer getNode() {
return getPropertyAsInteger(NODE);
}
public static Integer getNodeCount() {
return getPropertyAsInteger(NODE_COUNT);
}
static Integer getPropertyAsInteger(String property) {
try {
return Integer.decode(getProperty(property));
} catch (NumberFormatException | NullPointerException e) {
return null;
}
}
static String getProperty(String name) {
String property = System.getProperty(name);
if (property != null && !property.isEmpty()) {
return property;
}
String variable = System.getenv(name.toUpperCase().replace(".", "_"));
if (variable != null && !variable.isEmpty()) {
return variable;
}
return null;
}
}
/**
* Returns a new TSID.
*
* The node ID is is set by defining the system property "tsid.node" or
* the environment variable "TSID_NODE". One of them should be
* used to embed a machine ID in the generated TSID in order to avoid TSID
* collisions. If that property or variable is not defined, the node ID is
* chosen randomly.
*
* The amount of nodes can be set by defining the system property
* "tsid.node.count" or the environment variable
* "TSID_NODE_COUNT". That property or variable is used to adjust the
* minimum amount of bits to accommodate the node ID. If that property or
* variable is not defined, the default amount of nodes is 1024, which takes 10
* bits.
*
* The amount of bits needed to accommodate the node ID is calculated by this
* pseudo-code formula: {@code node_bits = ceil(log(node_count)/log(2))}.
*
* Random component settings:
*
* - Node bits: node_bits
*
- Counter bits: 22-node_bits
*
- Maximum node: 2^node_bits
*
- Maximum counter: 2^(22-node_bits)
*
*
* The time component can be 1 ms or more ahead of the system time when
* necessary to maintain monotonicity and generation speed.
*
* @return a TSID
* @since 5.1.0
*/
public static TSID getTsid() {
return INSTANCE.generate();
}
/**
* Returns a new TSID.
*
* It supports up to 256 nodes.
*
* It can generate up to 16,384 TSIDs per millisecond per node.
*
* The node ID is is set by defining the system property "tsid.node" or
* the environment variable "TSID_NODE". One of them should be
* used to embed a machine ID in the generated TSID in order to avoid TSID
* collisions. If that property or variable is not defined, the node ID is
* chosen randomly.
*
*
* Random component settings:
*
* - Node bits: 8
*
- Counter bits: 14
*
- Maximum node: 256 (2^8)
*
- Maximum counter: 16,384 (2^14)
*
*
* The time component can be 1 ms or more ahead of the system time when
* necessary to maintain monotonicity and generation speed.
*
* @return a TSID
*/
public static TSID getTsid256() {
return INSTANCE_256.generate();
}
/**
* Returns a new TSID.
*
* It supports up to 1,024 nodes.
*
* It can generate up to 4,096 TSIDs per millisecond per node.
*
* The node ID is is set by defining the system property "tsid.node" or
* the environment variable "TSID_NODE". One of them should be
* used to embed a machine ID in the generated TSID in order to avoid TSID
* collisions. If that property or variable is not defined, the node ID is
* chosen randomly.
*
* Random component settings:
*
* - Node bits: 10
*
- Counter bits: 12
*
- Maximum node: 1,024 (2^10)
*
- Maximum counter: 4,096 (2^12)
*
*
* The time component can be 1 ms or more ahead of the system time when
* necessary to maintain monotonicity and generation speed.
*
* @return a TSID
*/
public static TSID getTsid1024() {
return INSTANCE_1024.generate();
}
/**
* Returns a new TSID.
*
* It supports up to 4,096 nodes.
*
* It can generate up to 1,024 TSIDs per millisecond per node.
*
* The node ID is is set by defining the system property "tsid.node" or
* the environment variable "TSID_NODE". One of them should be
* used to embed a machine ID in the generated TSID in order to avoid TSID
* collisions. If that property or variable is not defined, the node ID is
* chosen randomly.
*
* Random component settings:
*
* - Node bits: 12
*
- Counter bits: 10
*
- Maximum node: 4,096 (2^12)
*
- Maximum counter: 1,024 (2^10)
*
*
* The time component can be 1 ms or more ahead of the system time when
* necessary to maintain monotonicity and generation speed.
*
* @return a TSID number
*/
public static TSID getTsid4096() {
return INSTANCE_4096.generate();
}
}
}