Skip to content

Commit

Permalink
feat: extract info from generated id, provide compress form of ID, up…
Browse files Browse the repository at this point in the history
…date README
  • Loading branch information
dotuancd committed Oct 6, 2021
1 parent 0adb7fb commit d29dd3b
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 39 deletions.
18 changes: 16 additions & 2 deletions README.md
Expand Up @@ -16,8 +16,15 @@ Add manual config to `application.conf`:
```hocon
snowflake4s {
twitter { # using Twitter's algorithm
machine_id = 1 # from 1 to 31
worker_id = 1 # from 1 to 31
machine_id = 1 # from 0 to 31
machine_id = ${?SNOWFLAKE4S_MACHINE_ID} # Set machine id from env
worker_id = 1 # from 0 to 31
worker_id = ${?SNOWFLAKE4S_WORKER_ID} # Set worker id from env
# Default Epoch is October 18, 1989, 16:53:40 UTC
# You can change to a different epoch by below setting
# epoch = "2021-01-01T00:00:00Z"
}
}
```
Expand All @@ -28,6 +35,13 @@ to generate id:
val IdGenerator = Snowflake4s.generator

val id = IdGenerator.generate()
id.toBase62 // 52nlGCNq00n
id.toLong // 4234436103643992065

// Revert info from saved Id

val id = Id.fromBase62("52nlGCNq00n")
println(id.workerId) // 5
```

You also could bulk generate 10 ids with the following snippet:
Expand Down
4 changes: 3 additions & 1 deletion build.sbt
Expand Up @@ -36,7 +36,9 @@ lazy val root = (project in file("."))
libraryDependencies ++= Seq(
"com.typesafe" % "config" % "1.3.1",
"com.google.inject" % "guice" % "4.1.0",
"net.codingwell" %% "scala-guice" % "4.2.9"
"net.codingwell" %% "scala-guice" % "4.2.9",
"com.github.tototoshi" % "scala-base62_2.10" % "0.1.0",
"org.scalatest" %% "scalatest" % "3.2.10" % Test
)
)
.settings(
Expand Down
51 changes: 51 additions & 0 deletions src/main/scala/com/septech/snowflake4s/Encoding.scala
@@ -0,0 +1,51 @@
package com.septech.snowflake4s

import java.time.{Instant, LocalDateTime, Month, ZoneId, ZonedDateTime}

private [snowflake4s] object Encoding {

/**
* This is a Custom Epoch, mean for reference time: October 18, 1989, 16:53:40 UTC -
* The date of Galileo Spacecraft was launched to explored Jupiter and its moon from Kennedy Space Center, Florida, US.
*
* Galileo is also the name used for the satellite navigation system of the European Union.
* It uses 22 August 1999 for Epoch instead of the Unix Epoch(January 1st, 1970).
*/
final val zoneId = ZoneId.of("US/Eastern")
final val GALILEO_LAUNCHED_DATETIME: ZonedDateTime =
LocalDateTime.of(1989, Month.OCTOBER, 18, 16, 53, 40).atZone(zoneId)

final val EPOCH: Long = Option(System.getProperty("snowflake4s.twitter.epoch"))
.map(ZonedDateTime.parse)
.map(_.toInstant.toEpochMilli)
.getOrElse(GALILEO_LAUNCHED_DATETIME.toInstant.toEpochMilli)

final private val workerIdBits: Long = 5L
final private val datacenterIdBits: Long = 5L
final private val sequenceBits: Long = 12L
final private val workerIdShift: Long = sequenceBits
final private val machineIdShift: Long = sequenceBits + workerIdBits
final private val timestampLeftShift: Long = sequenceBits + workerIdBits + datacenterIdBits
final private val sequenceMask: Long = -1L ^ (-1L << sequenceBits)

def encode(id: Id): Long = {
(id.timestamp - EPOCH) << timestampLeftShift |
id.machineId << machineIdShift |
id.workerId << workerIdShift |
id.counter
}

def decode(value: Long): Id = {
Id(
value >> sequenceBits & (-1L ^ (-1L << workerIdBits)),
(value >> machineIdShift) & (-1L ^ (-1L << datacenterIdBits)),
(value >> timestampLeftShift) + EPOCH,
value & sequenceMask
)
}

def getDateTime(id: Id): ZonedDateTime = {
Instant.ofEpochMilli(id.timestamp).atZone(zoneId)
}
}

4 changes: 2 additions & 2 deletions src/main/scala/com/septech/snowflake4s/Generator.scala
Expand Up @@ -17,8 +17,8 @@ package com.septech.snowflake4s

trait Generator {

def generate(): String
def generate(): Id

def bulkGenerate(batch: Int): List[String]
def bulkGenerate(batch: Int): List[Id]

}
33 changes: 33 additions & 0 deletions src/main/scala/com/septech/snowflake4s/Id.scala
@@ -0,0 +1,33 @@
package com.septech.snowflake4s

import java.time.ZonedDateTime
import com.github.tototoshi.base62.Base62

case class Id(workerId: Long, machineId: Long, timestamp: Long, counter: Long) {

def toLong: Long = encode

override def toString: String = encode.toString

def encode: Long = {
Encoding.encode(this)
}

def getDate: ZonedDateTime = {
Encoding.getDateTime(this)
}

def toBase62: String = new Base62().encode(encode)
}

object Id {
def from(id: Long): Id = {
Encoding.decode(id)
}

def fromBase62(id: String): Id = from(new Base62().decode(id))

def next: Id = Snowflake4s.generator.generate()

def bulk(numOfIds: Int): List[Id] = Snowflake4s.generator.bulkGenerate(numOfIds)
}
46 changes: 12 additions & 34 deletions src/main/scala/com/septech/snowflake4s/algorithms/Snowflake.scala
Expand Up @@ -16,17 +16,10 @@
package com.septech.snowflake4s.algorithms

import java.time.Clock
import java.time.LocalDateTime
import java.time.Month
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.concurrent.locks.ReentrantLock

import com.google.inject.Inject
import com.google.inject.Singleton
import com.septech.snowflake4s.Generator
import com.septech.snowflake4s.IdEntity
import com.septech.snowflake4s.MachineIdentifier
import com.septech.snowflake4s.{Generator, Id, MachineIdentifier}
import com.septech.snowflake4s.exception.GenerateException
import com.septech.snowflake4s.exception.InvalidSystemClock

Expand All @@ -37,49 +30,34 @@ import scala.util.Try

@Singleton
private[snowflake4s] class Snowflake @Inject()(identifier: MachineIdentifier) extends Generator {
/**
* This is a Custom Epoch, mean for reference time: October 18, 1989, 16:53:40 UTC -
* The date of Galileo Spacecraft was launched to explored Jupiter and its moon from Kennedy Space Center, Florida, US.
*
* Galileo is also the name used for the satellite navigation system of the European Union.
* It uses 22 August 1999 for Epoch instead of the Unix Epoch(January 1st, 1970).
*/
final val GALILEO_LAUNCHED_DATETIME: ZonedDateTime =
LocalDateTime.of(1989, Month.OCTOBER, 18, 16, 53, 40).atZone(ZoneId.of("US/Eastern"))
final val EPOCH: Long = GALILEO_LAUNCHED_DATETIME.toInstant.toEpochMilli

private final val STARTING_SEQUENCE_NUMBERS: Int = 1
private final val lock = new ReentrantLock()
private var sequence: Long = 0L

final private val workerIdBits: Long = 5L
final private val datacenterIdBits: Long = 5L
final private val sequenceBits: Long = 12L
final private val workerIdShift: Long = sequenceBits
final private val machineIdShift: Long = sequenceBits + workerIdBits
final private val timestampLeftShift: Long = sequenceBits + workerIdBits + datacenterIdBits
final private val sequenceMask: Long = -1L ^ (-1L << sequenceBits)
final private var lastTimestamp: Long = -1L

final private val MACHINE_ID: Long = identifier.getId.toLong
final private val WORKER_ID: Long = identifier.getWorkerId.toLong

override def generate(): String = bulkGenerate(1).headOption.fold[String](throw new GenerateException)(id => id)
override def generate(): Id = bulkGenerate(1).headOption.fold[Id](throw new GenerateException)(id => id)

override def bulkGenerate(batch: Int): List[String] = {
override def bulkGenerate(batch: Int): List[Id] = {
lock.lock()

val ids = generateIds(batch).map(_.getString)
val ids = generateIds(batch)

lock.unlock()
ids
}

private def generateIds(batch: Int): List[IdEntity] = synchronized {
private def generateIds(batch: Int): List[Id] = synchronized {
require(batch > 0, new IllegalArgumentException("batch must be a non negative number"))

Try {
val ids = new ListBuffer[IdEntity]()
val ids = new ListBuffer[Id]()

(STARTING_SEQUENCE_NUMBERS to batch).foreach(_ => ids += nextId)

Expand All @@ -90,7 +68,7 @@ private[snowflake4s] class Snowflake @Inject()(identifier: MachineIdentifier) ex
}
}

private def nextId: IdEntity = {
private def nextId: Id = {
var currentTimestamp: Long = Clock.systemUTC().millis()

if (currentTimestamp < lastTimestamp) {
Expand All @@ -110,10 +88,10 @@ private[snowflake4s] class Snowflake @Inject()(identifier: MachineIdentifier) ex

lastTimestamp = currentTimestamp

IdEntity(
(currentTimestamp - EPOCH) << timestampLeftShift |
MACHINE_ID << machineIdShift |
WORKER_ID << workerIdShift |
Id(
WORKER_ID,
MACHINE_ID,
currentTimestamp,
sequence
)
}
Expand All @@ -126,4 +104,4 @@ private[snowflake4s] class Snowflake @Inject()(identifier: MachineIdentifier) ex
timestamp
}

}
}
23 changes: 23 additions & 0 deletions src/test/scala/com/septech/snowflake4s/Snowflake4sTest.scala
@@ -0,0 +1,23 @@
package com.septech.snowflake4s

import org.scalatest.flatspec.AnyFlatSpec

import java.time.{LocalDateTime, ZoneId}

class Snowflake4sTest extends AnyFlatSpec {
it can "convert to various forms" in {

val timezone = ZoneId.of("US/Eastern")
val date = LocalDateTime.of(2021, 10, 15, 12, 30, 59, 127000000).atZone(timezone)
val timestamp = date.toInstant.toEpochMilli
// 1634315459127

val id = Id(1, 5, timestamp, 1)

assert(Id.from(4234436103643992065L) == id)
assert(id.toLong == 4234436103643992065L)
assert(id.getDate == date)
assert(id.toBase62 == "52nlGCNq00n")
assert(Id.fromBase62("52nlGCNq00n") == id)
}
}

0 comments on commit d29dd3b

Please sign in to comment.