Initial commit.
This commit is contained in:
66
idgen.go
Normal file
66
idgen.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package idgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base32"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var encoding = base32.StdEncoding.WithPadding(base32.NoPadding)
|
||||||
|
|
||||||
|
// Creates a new, random token from 20 random bytes, encoded as base32.
|
||||||
|
func NewToken() string {
|
||||||
|
buf := make([]byte, 20)
|
||||||
|
rand.Read(buf) // Guaranteed not to return an error.
|
||||||
|
return encoding.EncodeToString(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
lock sync.Mutex
|
||||||
|
ts int64 = time.Now().Unix()
|
||||||
|
seq int64 = 0
|
||||||
|
seqMax int64 = 1 << 20
|
||||||
|
)
|
||||||
|
|
||||||
|
// NextID can generate ~1M int64s per second.
|
||||||
|
//
|
||||||
|
// nodeID must be in [0, 63]. The counter is shared across all nodeIDs within a
|
||||||
|
// process, so IDs are not guaranteed to be sequential.
|
||||||
|
func NextID(nodeID int64) int64 {
|
||||||
|
if nodeID < 0 || nodeID >= 64 {
|
||||||
|
panic("nodeID out of range [0, 63]")
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if id := nextID(nodeID); id != 0 {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextID(nodeID int64) int64 {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
tt := time.Now().Unix()
|
||||||
|
if tt > ts {
|
||||||
|
ts = tt
|
||||||
|
seq = 1
|
||||||
|
} else {
|
||||||
|
seq++
|
||||||
|
if seq >= seqMax {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ts << 26) + (nodeID << 20) + seq
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitID(id int64) (unixTime, nodeID, counter int64) {
|
||||||
|
counter = id & (0x00000000000FFFFF)
|
||||||
|
nodeID = (id >> 20) & (0x000000000000003F)
|
||||||
|
unixTime = id >> 26
|
||||||
|
return
|
||||||
|
}
|
||||||
125
idgen_test.go
Normal file
125
idgen_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package idgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewToken_length(t *testing.T) {
|
||||||
|
tok := NewToken()
|
||||||
|
if len(tok) != 32 {
|
||||||
|
t.Fatalf("expected 32 chars, got %d: %q", len(tok), tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewToken_validBase32(t *testing.T) {
|
||||||
|
tok := NewToken()
|
||||||
|
if _, err := encoding.DecodeString(tok); err != nil {
|
||||||
|
t.Fatalf("invalid base32: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewToken_unique(t *testing.T) {
|
||||||
|
seen := make(map[string]bool, 100)
|
||||||
|
for range 100 {
|
||||||
|
tok := NewToken()
|
||||||
|
if seen[tok] {
|
||||||
|
t.Fatal("duplicate token generated")
|
||||||
|
}
|
||||||
|
seen[tok] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextID_roundtrip(t *testing.T) {
|
||||||
|
const nodeID = 42
|
||||||
|
before := time.Now().Unix()
|
||||||
|
id := NextID(nodeID)
|
||||||
|
after := time.Now().Unix()
|
||||||
|
|
||||||
|
gotTime, gotNode, gotSeq := SplitID(id)
|
||||||
|
if gotNode != nodeID {
|
||||||
|
t.Errorf("nodeID: got %d, want %d", gotNode, nodeID)
|
||||||
|
}
|
||||||
|
if gotTime < before || gotTime > after {
|
||||||
|
t.Errorf("timestamp %d out of range [%d, %d]", gotTime, before, after)
|
||||||
|
}
|
||||||
|
if gotSeq < 1 {
|
||||||
|
t.Errorf("seq should be >= 1, got %d", gotSeq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextID_allNodeIDs(t *testing.T) {
|
||||||
|
for nodeID := int64(0); nodeID < 64; nodeID++ {
|
||||||
|
id := NextID(nodeID)
|
||||||
|
_, got, _ := SplitID(id)
|
||||||
|
if got != nodeID {
|
||||||
|
t.Errorf("nodeID %d: SplitID returned %d", nodeID, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextID_monotonic(t *testing.T) {
|
||||||
|
prev := NextID(0)
|
||||||
|
for range 1000 {
|
||||||
|
id := NextID(0)
|
||||||
|
if id <= prev {
|
||||||
|
t.Fatalf("not monotonically increasing: %d <= %d", id, prev)
|
||||||
|
}
|
||||||
|
prev = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextID_unique(t *testing.T) {
|
||||||
|
seen := make(map[int64]bool, 1000)
|
||||||
|
for i := range 1000 {
|
||||||
|
id := NextID(0)
|
||||||
|
if seen[id] {
|
||||||
|
t.Fatalf("duplicate ID at i=%d", i)
|
||||||
|
}
|
||||||
|
seen[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextID_invalidNodeID(t *testing.T) {
|
||||||
|
for _, bad := range []int64{-1, 64, 100} {
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if recover() == nil {
|
||||||
|
t.Errorf("NextID(%d) should have panicked", bad)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
NextID(bad)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextID_timestampAdvances(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
// synctest's synthetic clock starts at 2000-01-01 00:00:00 UTC.
|
||||||
|
// Prime ts to one second before that so the first NextID call
|
||||||
|
// enters the new-second branch and records the synthetic start time.
|
||||||
|
lock.Lock()
|
||||||
|
ts = time.Now().Unix() - 1
|
||||||
|
seq = 0
|
||||||
|
lock.Unlock()
|
||||||
|
|
||||||
|
id1 := NextID(0)
|
||||||
|
t1, _, _ := SplitID(id1)
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
id2 := NextID(0)
|
||||||
|
t2, _, _ := SplitID(id2)
|
||||||
|
|
||||||
|
if t2 <= t1 {
|
||||||
|
t.Errorf("timestamp did not advance: %d -> %d", t1, t2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNextID(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
NextID(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user