Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add randomness pool mode for V4 UUID #80

Merged
merged 2 commits into from Jul 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 38 additions & 1 deletion uuid.go
Expand Up @@ -12,6 +12,7 @@ import (
"fmt"
"io"
"strings"
"sync"
)

// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
Expand All @@ -33,7 +34,15 @@ const (
Future // Reserved for future definition.
)

var rander = rand.Reader // random function
const randPoolSize = 16 * 16

var (
rander = rand.Reader // random function
poolEnabled = false
poolMu sync.Mutex
poolPos = randPoolSize // protected with poolMu
pool [randPoolSize]byte // protected with poolMu
)

type invalidLengthError struct{ len int }

Expand Down Expand Up @@ -255,3 +264,31 @@ func SetRand(r io.Reader) {
}
rander = r
}

// EnableRandPool enables internal randomness pool used for Random
// (Version 4) UUID generation. The pool contains random bytes read from
// the random number generator on demand in batches. Enabling the pool
// may improve the UUID generation throughput significantly.
//
// Since the pool is stored on the Go heap, this feature may be a bad fit
// for security sensitive applications.
//
// Both EnableRandPool and DisableRandPool are not thread-safe and should
// only be called when there is no possibility that New or any other
// UUID Version 4 generation function will be called concurrently.
func EnableRandPool() {
puzpuzpuz marked this conversation as resolved.
Show resolved Hide resolved
poolEnabled = true
}

// DisableRandPool disables the randomness pool if it was previously
// enabled with EnableRandPool.
//
// Both EnableRandPool and DisableRandPool are not thread-safe and should
// only be called when there is no possibility that New or any other
// UUID Version 4 generation function will be called concurrently.
func DisableRandPool() {
poolEnabled = false
defer poolMu.Unlock()
poolMu.Lock()
poolPos = randPoolSize
}
59 changes: 59 additions & 0 deletions uuid_test.go
Expand Up @@ -179,6 +179,26 @@ func TestRandomUUID(t *testing.T) {
}
}

func TestRandomUUID_Pooled(t *testing.T) {
defer DisableRandPool()
EnableRandPool()
m := make(map[string]bool)
for x := 1; x < 128; x++ {
uuid := New()
s := uuid.String()
if m[s] {
t.Errorf("NewRandom returned duplicated UUID %s", s)
}
m[s] = true
if v := uuid.Version(); v != 4 {
t.Errorf("Random UUID of version %s", v)
}
if uuid.Variant() != RFC4122 {
t.Errorf("Random UUID is variant %d", uuid.Variant())
}
}
}

func TestNew(t *testing.T) {
m := make(map[UUID]bool)
for x := 1; x < 32; x++ {
Expand Down Expand Up @@ -517,6 +537,22 @@ func TestRandomFromReader(t *testing.T) {
}
}

func TestRandPool(t *testing.T) {
myString := "8059ddhdle77cb52"
EnableRandPool()
SetRand(strings.NewReader(myString))
_, err := NewRandom()
if err == nil {
t.Errorf("expecting an error as reader has no more bytes")
}
DisableRandPool()
SetRand(strings.NewReader(myString))
_, err = NewRandom()
if err != nil {
t.Errorf("failed generating UUID from a reader")
}
}

func TestWrongLength(t *testing.T) {
_, err := Parse("12345")
if err == nil {
Expand Down Expand Up @@ -641,3 +677,26 @@ func BenchmarkParseLen36Corrupted(b *testing.B) {
}
}
}

func BenchmarkUUID_New(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := NewRandom()
if err != nil {
b.Fatal(err)
}
}
})
}

func BenchmarkUUID_NewPooled(b *testing.B) {
EnableRandPool()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := NewRandom()
if err != nil {
b.Fatal(err)
}
}
})
}
27 changes: 26 additions & 1 deletion version4.go
Expand Up @@ -27,6 +27,8 @@ func NewString() string {
// The strength of the UUIDs is based on the strength of the crypto/rand
// package.
//
// Uses the randomness pool if it was enabled with EnableRandPool.
//
// A note about uniqueness derived from the UUID Wikipedia entry:
//
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
Expand All @@ -35,7 +37,10 @@ func NewString() string {
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
// year and having one duplicate.
func NewRandom() (UUID, error) {
return NewRandomFromReader(rander)
if !poolEnabled {
return NewRandomFromReader(rander)
}
return newRandomFromPool()
}

// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
Expand All @@ -49,3 +54,23 @@ func NewRandomFromReader(r io.Reader) (UUID, error) {
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}

func newRandomFromPool() (UUID, error) {
var uuid UUID
poolMu.Lock()
if poolPos == randPoolSize {
_, err := io.ReadFull(rander, pool[:])
if err != nil {
poolMu.Unlock()
return Nil, err
}
poolPos = 0
}
copy(uuid[:], pool[poolPos:(poolPos+16)])
poolPos += 16
poolMu.Unlock()

uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}