diff --git a/uuid.go b/uuid.go index d732ae8..a57207a 100644 --- a/uuid.go +++ b/uuid.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "strings" + "sync" ) // A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC @@ -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 } @@ -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() { + 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 +} diff --git a/uuid_test.go b/uuid_test.go index 99f0bed..e98d0fe 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -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++ { @@ -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 { @@ -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) + } + } + }) +} diff --git a/version4.go b/version4.go index 86160fb..7697802 100644 --- a/version4.go +++ b/version4.go @@ -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 @@ -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. @@ -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 +}