Skip to content

Commit 655bf50

Browse files
authored
Add randomness pool mode for V4 UUID (#80)
* Add randomness pool mode for V4 UUID Adds an optional randomness pool mode 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. That's why it's implemented as an opt-in feature. * fixup! document thread-safety aspects
1 parent 512b657 commit 655bf50

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-2
lines changed

uuid.go

+38-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"fmt"
1313
"io"
1414
"strings"
15+
"sync"
1516
)
1617

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

36-
var rander = rand.Reader // random function
37+
const randPoolSize = 16 * 16
38+
39+
var (
40+
rander = rand.Reader // random function
41+
poolEnabled = false
42+
poolMu sync.Mutex
43+
poolPos = randPoolSize // protected with poolMu
44+
pool [randPoolSize]byte // protected with poolMu
45+
)
3746

3847
type invalidLengthError struct{ len int }
3948

@@ -255,3 +264,31 @@ func SetRand(r io.Reader) {
255264
}
256265
rander = r
257266
}
267+
268+
// EnableRandPool enables internal randomness pool used for Random
269+
// (Version 4) UUID generation. The pool contains random bytes read from
270+
// the random number generator on demand in batches. Enabling the pool
271+
// may improve the UUID generation throughput significantly.
272+
//
273+
// Since the pool is stored on the Go heap, this feature may be a bad fit
274+
// for security sensitive applications.
275+
//
276+
// Both EnableRandPool and DisableRandPool are not thread-safe and should
277+
// only be called when there is no possibility that New or any other
278+
// UUID Version 4 generation function will be called concurrently.
279+
func EnableRandPool() {
280+
poolEnabled = true
281+
}
282+
283+
// DisableRandPool disables the randomness pool if it was previously
284+
// enabled with EnableRandPool.
285+
//
286+
// Both EnableRandPool and DisableRandPool are not thread-safe and should
287+
// only be called when there is no possibility that New or any other
288+
// UUID Version 4 generation function will be called concurrently.
289+
func DisableRandPool() {
290+
poolEnabled = false
291+
defer poolMu.Unlock()
292+
poolMu.Lock()
293+
poolPos = randPoolSize
294+
}

uuid_test.go

+59
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,26 @@ func TestRandomUUID(t *testing.T) {
179179
}
180180
}
181181

182+
func TestRandomUUID_Pooled(t *testing.T) {
183+
defer DisableRandPool()
184+
EnableRandPool()
185+
m := make(map[string]bool)
186+
for x := 1; x < 128; x++ {
187+
uuid := New()
188+
s := uuid.String()
189+
if m[s] {
190+
t.Errorf("NewRandom returned duplicated UUID %s", s)
191+
}
192+
m[s] = true
193+
if v := uuid.Version(); v != 4 {
194+
t.Errorf("Random UUID of version %s", v)
195+
}
196+
if uuid.Variant() != RFC4122 {
197+
t.Errorf("Random UUID is variant %d", uuid.Variant())
198+
}
199+
}
200+
}
201+
182202
func TestNew(t *testing.T) {
183203
m := make(map[UUID]bool)
184204
for x := 1; x < 32; x++ {
@@ -517,6 +537,22 @@ func TestRandomFromReader(t *testing.T) {
517537
}
518538
}
519539

540+
func TestRandPool(t *testing.T) {
541+
myString := "8059ddhdle77cb52"
542+
EnableRandPool()
543+
SetRand(strings.NewReader(myString))
544+
_, err := NewRandom()
545+
if err == nil {
546+
t.Errorf("expecting an error as reader has no more bytes")
547+
}
548+
DisableRandPool()
549+
SetRand(strings.NewReader(myString))
550+
_, err = NewRandom()
551+
if err != nil {
552+
t.Errorf("failed generating UUID from a reader")
553+
}
554+
}
555+
520556
func TestWrongLength(t *testing.T) {
521557
_, err := Parse("12345")
522558
if err == nil {
@@ -641,3 +677,26 @@ func BenchmarkParseLen36Corrupted(b *testing.B) {
641677
}
642678
}
643679
}
680+
681+
func BenchmarkUUID_New(b *testing.B) {
682+
b.RunParallel(func(pb *testing.PB) {
683+
for pb.Next() {
684+
_, err := NewRandom()
685+
if err != nil {
686+
b.Fatal(err)
687+
}
688+
}
689+
})
690+
}
691+
692+
func BenchmarkUUID_NewPooled(b *testing.B) {
693+
EnableRandPool()
694+
b.RunParallel(func(pb *testing.PB) {
695+
for pb.Next() {
696+
_, err := NewRandom()
697+
if err != nil {
698+
b.Fatal(err)
699+
}
700+
}
701+
})
702+
}

version4.go

+26-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ func NewString() string {
2727
// The strength of the UUIDs is based on the strength of the crypto/rand
2828
// package.
2929
//
30+
// Uses the randomness pool if it was enabled with EnableRandPool.
31+
//
3032
// A note about uniqueness derived from the UUID Wikipedia entry:
3133
//
3234
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
@@ -35,7 +37,10 @@ func NewString() string {
3537
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
3638
// year and having one duplicate.
3739
func NewRandom() (UUID, error) {
38-
return NewRandomFromReader(rander)
40+
if !poolEnabled {
41+
return NewRandomFromReader(rander)
42+
}
43+
return newRandomFromPool()
3944
}
4045

4146
// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
@@ -49,3 +54,23 @@ func NewRandomFromReader(r io.Reader) (UUID, error) {
4954
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
5055
return uuid, nil
5156
}
57+
58+
func newRandomFromPool() (UUID, error) {
59+
var uuid UUID
60+
poolMu.Lock()
61+
if poolPos == randPoolSize {
62+
_, err := io.ReadFull(rander, pool[:])
63+
if err != nil {
64+
poolMu.Unlock()
65+
return Nil, err
66+
}
67+
poolPos = 0
68+
}
69+
copy(uuid[:], pool[poolPos:(poolPos+16)])
70+
poolPos += 16
71+
poolMu.Unlock()
72+
73+
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
74+
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
75+
return uuid, nil
76+
}

0 commit comments

Comments
 (0)