Skip to content

Commit

Permalink
feat: updated sequence resolver and added more tests for 100% coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
HotPotatoC committed Jan 3, 2022
1 parent 27143ea commit 7720048
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 53 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
coverage.html

# Dependency directories (remove the comment below to include it)
# vendor/
Expand Down
21 changes: 0 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,6 @@ fmt.Printf("Machine ID: %d\n", parsed.Discriminator1) // 1
fmt.Printf("Process ID: %d\n", parsed.Discriminator2) // 24
```

## Performance

> Benched on Windows 10 - WSL 2 Ubuntu, Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz and 12GB of memory
```bash
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkNewID
BenchmarkNewID/github.com/HotPotatoC/snowflake
BenchmarkNewID/github.com/HotPotatoC/snowflake-8 14012617 87.52 ns/op 0 B/op 0 allocs/op
BenchmarkNewID/github.com/bwmarrin/snowflake
BenchmarkNewID/github.com/bwmarrin/snowflake-8 4918552 244.3 ns/op 0 B/op 0 allocs/op
BenchmarkNewID/github.com/godruoyi/go-snowflake
BenchmarkNewID/github.com/godruoyi/go-snowflake-8 4916791 245.8 ns/op 0 B/op 0 allocs/op
PASS
ok command-line-arguments 4.230s
```

Disclaimer: Benchmark results may be faster than other implementations. But this library has not been fully tested and corner cases may be missed.

## Support

<a href="https://www.buymeacoffee.com/hotpotato" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
83 changes: 51 additions & 32 deletions snowflake.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ const (
)

var (
// epoch defaults to March 3, 2012, 00:00:00 UTC. Which is
// the release date of Go 1.0.
// You can customize it by calling SetEpoch()
// // Example
// err := snowflake.SetEpoch(time.Date(2009, 1, 1, 0, 0, 0, 0, time.UTC))
epoch = time.Date(2012, 3, 28, 0, 0, 0, 0, time.UTC)

// ErrEpochIsZero is returned when the epoch is set to the zero time.
Expand All @@ -28,6 +23,14 @@ var (
ErrEpochFuture = errors.New("epoch is in the future")
)

// Epoch returns the current configured epoch.
// epoch defaults to March 3, 2012, 00:00:00 UTC. Which is
// the release date of Go 1.0.
// You can customize it by calling SetEpoch()
// // Example setting the epoch to 2010-01-01 00:00:00 UTC
// err := snowflake.SetEpoch(time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC))
func Epoch() time.Time { return epoch }

// SetEpoch changes the epoch / starting time to a custom time.
// Can return 2 errors: ErrEpochIsZero and ErrEpochFuture.
// If the epoch is set to the zero time, ErrEpochIsZero is returned.
Expand All @@ -54,11 +57,12 @@ type ID struct {
discriminator uint64
sequence uint64
elapsedTime int64
lastID uint64
}

// New returns a new snowflake.ID (max discriminator value: 1023)
func New(discriminator uint64) *ID {
return &ID{discriminator: discriminator}
return &ID{discriminator: discriminator, lastID: 0}
}

// NextID returns a new snowflake ID.
Expand All @@ -70,23 +74,24 @@ func (id *ID) NextID() uint64 {
id.mtx.Lock()
defer id.mtx.Unlock()

nowSinceEpoch := time.Since(epoch).Nanoseconds() / int64(time.Millisecond)
nowSinceEpoch := msSinceEpoch()

// reference: https://github.com/twitter-archive/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala#L81
if nowSinceEpoch == id.elapsedTime { // same millisecond as last time
id.sequence = (id.sequence + 1) & maxSeqBits // increment sequence number

// If this is the first call to NextID(), initialize the elapsedTime
// and set sequence to zero.
if id.elapsedTime < nowSinceEpoch {
id.elapsedTime = nowSinceEpoch
id.sequence = 0
} else { // Otherwise, increment the sequence number.
id.sequence = (id.sequence + 1) & maxSeqBits
// if we've used up all the bits in the sequence number,
// we need to increment the timestamp
if id.sequence == 0 {
id.elapsedTime++
// if we've used up all the bits in the sequence number,
// we need to change the timestamp
nowSinceEpoch = waitUntilNextMs(id.elapsedTime) // wait until next millisecond
}
} else {
id.sequence = 0
}

timestampSegment := uint64(nowSinceEpoch << (sequenceBits + discriminatorBits))
id.elapsedTime = nowSinceEpoch

timestampSegment := uint64(id.elapsedTime << (sequenceBits + discriminatorBits))
discriminatorSegment := uint64(id.discriminator) << sequenceBits
sequenceSegment := uint64(id.sequence)

Expand Down Expand Up @@ -141,23 +146,24 @@ func (id *ID2) NextID() uint64 {
id.mtx.Lock()
defer id.mtx.Unlock()

nowSinceEpoch := time.Since(epoch).Nanoseconds() / int64(time.Millisecond)
nowSinceEpoch := msSinceEpoch()

// reference: https://github.com/twitter-archive/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala#L81
if nowSinceEpoch == id.elapsedTime { // same millisecond as last time
id.sequence = (id.sequence + 1) & maxSeqBits // increment sequence number

// If this is the first call to NextID(), initialize the elapsedTime
// and set sequence to zero.
if id.elapsedTime < nowSinceEpoch {
id.elapsedTime = nowSinceEpoch
id.sequence = 0
} else { // Otherwise, increment the sequence number.
id.sequence = (id.sequence + 1) & maxSeqBits
// if we've used up all the bits in the sequence number,
// we need to increment the timestamp
if id.sequence == 0 {
id.elapsedTime++
// if we've used up all the bits in the sequence number,
// we need to change the timestamp
nowSinceEpoch = waitUntilNextMs(id.elapsedTime) // wait until next millisecond
}
} else {
id.sequence = 0
}

timestampSegment := uint64(nowSinceEpoch << (sequenceBits + discriminatorBits))
id.elapsedTime = nowSinceEpoch

timestampSegment := uint64(id.elapsedTime << (sequenceBits + discriminatorBits))
discriminator1Segment := id.discriminator1 << uint64(sequenceBits)
discriminator2Segment := id.discriminator2 << uint64(sequenceBits+discriminatorBits/2)
sequenceSegment := uint64(id.sequence)
Expand Down Expand Up @@ -196,6 +202,20 @@ func Parse2(sid uint64) SID2 {
}
}

// waitUntilNextMs waits until the next millisecond to return. (internal-use only)
func waitUntilNextMs(last int64) int64 {
ms := msSinceEpoch()
for ms <= last {
ms = msSinceEpoch()
}
return ms
}

// msSinceEpoch returns the number of milliseconds since the epoch. (internal-use only)
func msSinceEpoch() int64 {
return time.Since(epoch).Nanoseconds() / 1e6
}

// getDiscriminant returns the discriminant value of a snowflake ID. (internal-use only)
func getDiscriminant(id uint64) uint64 {
return (id >> sequenceBits) & maxDiscriminatorBits
Expand All @@ -213,8 +233,7 @@ func getSecondDiscriminant(id uint64) uint64 {

// getTimestamp returns the timestamp of a snowflake ID. (internal-use only)
func getTimestamp(id uint64) int64 {
shift := sequenceBits + discriminatorBits
return int64(id>>shift) + epoch.UnixNano()/int64(time.Millisecond)
return int64(id>>(sequenceBits+discriminatorBits)) + epoch.UnixNano()/1e6
}

// getSequence returns the sequence number of a snowflake ID. (internal-use only)
Expand Down

0 comments on commit 7720048

Please sign in to comment.