Last active
October 6, 2018 06:06
-
-
Save Tvaroh/9b50295e8e36ddf03a86 to your computer and use it in GitHub Desktop.
Flake UUID generator in Scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package io.treev.common.util | |
import java.math.BigInteger | |
import java.net.NetworkInterface | |
import java.nio.ByteBuffer | |
import java.util | |
import java.util.Base64 | |
import java.util.concurrent.atomic.AtomicReference | |
/** UUID in 'flake' format. | |
* <p>Flake ids are 128-bits wide described here from most significant to least significant bits. | |
* <ul> | |
* <li>64-bit timestamp - milliseconds since the epoch (Jan 1 1970);</li> | |
* <li>48-bit worker id - MAC address from a configurable device;</li> | |
* <li> | |
* 16-bit sequence number - usually 0, incremented when more than one id is requested in the same millisecond | |
* and reset to 0 when the clock ticks forward. | |
* </li> | |
* </ul> */ | |
trait UUID extends Serializable { | |
/** Get time part in milliseconds since the epoch (Jan 1 1970). */ | |
def getTime: Long | |
/** Get MAC address part. */ | |
def getMac: Array[Byte] | |
/** Get sequence part. */ | |
def getSequence: Short | |
/** Get string representation in standard format. | |
* Delegates to [[java.util.UUID]]. */ | |
def toString: String | |
/** Get UUID value as byte array. */ | |
def toBytes: Array[Byte] | |
/** Get base-64 representation (22 characters). Includes no padding. */ | |
def toBase64: String | |
/** Convert to [[java.util.UUID]]. */ | |
def toJavaUUID: java.util.UUID | |
} | |
/** UUID implementation. | |
* @param time underlying time component | |
* @param mac underlying hardware address | |
* @param sequence underlying sequence component */ | |
private class UUIDImpl(time: Long, mac: Array[Byte], sequence: Short) extends UUID { | |
/** Compare for equality. */ | |
override def equals(obj: Any): Boolean = obj match { | |
case that: UUID ⇒ | |
that.getTime == this.time && util.Arrays.equals(that.getMac, this.mac) && that.getSequence == this.sequence | |
case _ ⇒ | |
false | |
} | |
/** Get UUID hashcode. | |
* Delegates to [[java.util.UUID]]. */ | |
override def hashCode: Int = | |
toJavaUUID.hashCode() | |
/** Get string representation in standard format. | |
* Delegates to [[java.util.UUID]]. */ | |
override def toString: String = | |
toJavaUUID.toString | |
/** Get UUID value as byte array. */ | |
@transient override lazy val toBytes: Array[Byte] = UUID.toBytes(time, mac, sequence) | |
/** Get base-64 representation (22 characters). Includes no padding. */ | |
override def toBase64: String = UUID.base64Encoder.encodeToString(toBytes) | |
/** Convert to [[java.util.UUID]]. */ | |
override def toJavaUUID: java.util.UUID = { | |
val buf = ByteBuffer.wrap(toBytes) | |
new java.util.UUID(buf.getLong(), buf.getLong()) | |
} | |
/** Get time part in milliseconds since the epoch (Jan 1 1970). */ | |
override def getTime: Long = time | |
/** Get MAC address part. */ | |
override def getMac: Array[Byte] = util.Arrays.copyOf(mac, mac.length) | |
/** Get sequence part. */ | |
override def getSequence: Short = sequence | |
} | |
object UUID { | |
/** Generate new UUID. | |
* @see [[io.treev.common.util.UUID]] */ | |
def apply(): UUID = { | |
val TimeAndSequence(time, sequence) = genTimeAndSequence() | |
new UUIDImpl(time, MacAddress, sequence) | |
} | |
/** Hand-craft a UUID. | |
* @param time underlying time component | |
* @param mac underlying hardware address (6 bytes) | |
* @param sequence underlying sequence component | |
* @see [[io.treev.common.util.UUID]] */ | |
def apply(time: Long, mac: Array[Byte], sequence: Short): UUID = { | |
require(mac.length == MacAddressSize, s"MAC address should be $MacAddressSize bytes wide") | |
new UUIDImpl(time, mac, sequence) | |
} | |
/** Parse UUID from string representation. | |
* Delegates to [[java.util.UUID]]. */ | |
def apply(name: String): UUID = { | |
val javaUUID = java.util.UUID.fromString(name) | |
fromJavaUUID(javaUUID) | |
} | |
/** Create UUID from byte array. | |
* @param bytes byte array of size 16 | |
* @see [[io.treev.common.util.UUID]] */ | |
def fromBytes(bytes: Array[Byte]): UUID = { | |
require(bytes.length == UUIDSize, s"UUID should be $UUIDSize bytes wide") | |
val buf = ByteBuffer.wrap(bytes) | |
val mac = new Array[Byte](MacAddressSize) | |
val time = buf.getLong() | |
buf.get(mac) | |
val sequence = buf.getShort() | |
apply(time, mac, sequence) | |
} | |
/** Create UUID from base64-encoded string | |
* @param s base64-encoded string */ | |
def fromBase64(s: String): UUID = { | |
val bytes = base64Decoder.decode(s) | |
fromBytes(bytes) | |
} | |
/** Create UUID from Java UUID. | |
* @param javaUUID java UUID */ | |
def fromJavaUUID(javaUUID: java.util.UUID): UUID = { | |
val buf = buffer.get() | |
buf.rewind() | |
buf.putLong(javaUUID.getMostSignificantBits) | |
buf.putLong(javaUUID.getLeastSignificantBits) | |
fromBytes(buf.array()) | |
} | |
/** Implicit ordering for [[io.treev.common.util.UUID]] values. */ | |
implicit object uuidOrdering extends Ordering[UUID] { | |
override def compare(x: UUID, y: UUID): Int = | |
if (x eq y) 0 | |
else { | |
if (x.getTime > y.getTime) 1 | |
else if (x.getTime < y.getTime) -1 | |
else { | |
val result = new BigInteger(x.getMac).compareTo(new BigInteger(y.getMac)) | |
if (result != 0) result | |
else { | |
if (x.getSequence > y.getSequence) 1 | |
else if (x.getSequence < y.getSequence) -1 | |
else 0 | |
} | |
} | |
} | |
} | |
private[util] val base64Encoder = Base64.getEncoder.withoutPadding() | |
private[util] val base64Decoder = Base64.getDecoder | |
private val UUIDSize = 16 | |
private[util] def toBytes(time: Long, mac: Array[Byte], sequence: Short): Array[Byte] = { | |
val buf = buffer.get() | |
buf.rewind() | |
buf.putLong(time) | |
buf.put(mac) | |
buf.putShort(sequence) | |
buf.rewind() | |
val arr = new Array[Byte](UUIDSize) | |
buf.get(arr) | |
arr | |
} | |
// MAC address part | |
private val MacAddressSize = 6 | |
private lazy val MacAddress: Array[Byte] = { | |
import scala.collection.JavaConversions._ | |
val interfaces = NetworkInterface.getNetworkInterfaces | |
interfaces.find { interface ⇒ | |
val mac = interface.getHardwareAddress | |
mac != null && mac.length == MacAddressSize && mac(1) != 0xff | |
} map { | |
_.getHardwareAddress | |
} getOrElse { | |
sys.error("Cannot initialize flake id generator: MAC address not found") | |
} | |
} | |
// time and sequence parts | |
private case class TimeAndSequence(time: Long, sequence: Short) { | |
def incrementSequence: TimeAndSequence = | |
if (sequence < Short.MaxValue) this.copy(sequence = (sequence + 1).toShort) | |
else sys.error("Sequence value overflow") | |
} | |
private val lastTimeAndSequence = new AtomicReference[TimeAndSequence](TimeAndSequence(0L, 0)) | |
@annotation.tailrec | |
private def genTimeAndSequence(): TimeAndSequence = { | |
val currentLastTimeAndSequence = lastTimeAndSequence.get() | |
val currentTime = System.currentTimeMillis() | |
if (currentTime > currentLastTimeAndSequence.time) { | |
val nextTimeAndSequence = TimeAndSequence(currentTime, 0) | |
if (lastTimeAndSequence.compareAndSet(currentLastTimeAndSequence, nextTimeAndSequence)) nextTimeAndSequence | |
else genTimeAndSequence() | |
} else if (currentTime == currentLastTimeAndSequence.time) { | |
val nextTimeAndSequence = currentLastTimeAndSequence.incrementSequence | |
if (lastTimeAndSequence.compareAndSet(currentLastTimeAndSequence, nextTimeAndSequence)) nextTimeAndSequence | |
else genTimeAndSequence() | |
} else { | |
genTimeAndSequence() | |
} | |
} | |
// per-thread byte buffer | |
private val buffer: ThreadLocal[ByteBuffer] = new ThreadLocal[ByteBuffer] { | |
override def initialValue(): ByteBuffer = ByteBuffer.allocate(UUIDSize) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example UUIDs: